輕量函數(shù)式 JavaScript 第十一章:綜合應(yīng)用

感謝社區(qū)中各位的大力支持,譯者再次奉上一點(diǎn)點(diǎn)福利:阿里云產(chǎn)品券晌梨,享受所有官網(wǎng)優(yōu)惠桥嗤,并抽取幸運(yùn)大獎(jiǎng):點(diǎn)擊這里領(lǐng)取

現(xiàn)在须妻,你擁有了為了理解輕量函數(shù)式 JavaScript 所需的一切。再?zèng)]有新的概念要介紹了泛领。

在這最后的一章中荒吏,我們的目標(biāo)是凝聚這些概念。我們將看到將這本書(shū)中的許多主題融合在一起的代碼 —— 應(yīng)用我們學(xué)到的東西渊鞋。最重要的是绰更,這篇代碼示例是為了展示 “輕量函數(shù)式” 應(yīng)用到 JavaScript 上的方式 —— 也就是,平衡以及教條之上的實(shí)用主義锡宋。

你將會(huì)想要廣泛地親自實(shí)踐這些技術(shù)儡湾。消化理解這一章對(duì)于你將 FP 的概念應(yīng)用于真實(shí)世界的代碼至關(guān)重要。

準(zhǔn)備

讓我們建造一個(gè)簡(jiǎn)單的證券報(bào)價(jià)機(jī)控件执俩。

注意: 為了便于引用徐钠,這個(gè)示例的全部代碼位于 ch11-code/ 子目錄中 —— 參見(jiàn)這本書(shū)的 GitHub 代碼庫(kù) (https://github.com/getify/Functional-Light-JS)。另外役首,這個(gè)示例需要選用一些我們?cè)谶@本書(shū)中討論過(guò)的 FP 幫助函數(shù)尝丐,它們包含在 ch11-code/fp-helpers.js 中。本章中我們僅將注意力集中在與我們的討論有關(guān)的部分代碼中衡奥。

首先爹袁,讓我談?wù)勥@個(gè)控件的置標(biāo)代碼,這樣我們才有地方來(lái)展示信息杰赛。我們從 ch11-code/index.html 文件中的一個(gè)空 <ul ..> 元素開(kāi)始,當(dāng)運(yùn)行的時(shí)候矮台,DOM 將會(huì)被填充為:

<ul id="stock-ticker">
    <li class="stock" data-stock-id="AAPL">
        <span class="stock-name">AAPL</span>
        <span class="stock-price">$121.95</span>
        <span class="stock-change">+0.01</span>
    </li>
    <li class="stock" data-stock-id="MSFT">
        <span class="stock-name">MSFT</span>
        <span class="stock-price">$65.78</span>
        <span class="stock-change">+1.51</span>
    </li>
    <li class="stock" data-stock-id="GOOG">
        <span class="stock-name">GOOG</span>
        <span class="stock-price">$821.31</span>
        <span class="stock-change">-8.84</span>
    </li>
</ul>

在我們前進(jìn)之前乏屯,讓我提醒你一下:與 DOM 的交互是一種 I/O,而這意味著副作用瘦赫。我們不能消滅這些副作用辰晕,但可以限制并控制它們。我們要確實(shí)有意地將我們的應(yīng)用程序處理 DOM 的表面積控制在最小确虱。我們已經(jīng)在第五章中學(xué)習(xí)過(guò)這些技術(shù)了含友。

概括一下我們控件的功能:這段代碼將會(huì)在每次 “收到” 新證券事件時(shí)添加一個(gè) <li ..> 元素,并在證券更新事件到來(lái)時(shí)更新它的價(jià)格校辩。

在第十一章的示例代碼窘问,ch11-code/mock-server.js 中,我們?cè)O(shè)立了某種定時(shí)器宜咒,它隨機(jī)地向一個(gè)簡(jiǎn)單事件發(fā)生器推送虛構(gòu)的證券數(shù)據(jù)惠赫,來(lái)模擬我們正在從一個(gè)服務(wù)器接受證券信息。我們暴露了一個(gè) connectToServer() 函數(shù)假裝這樣做故黑,但實(shí)際上只是一個(gè)虛構(gòu)的事件發(fā)生器實(shí)例儿咱。

注意: 這個(gè)文件中都是虛構(gòu)/模擬的行為庭砍,所以我沒(méi)有花費(fèi)太多的力氣來(lái)使它支持 FP。我不建議你花太多的時(shí)間來(lái)關(guān)心這個(gè)文件中的代碼混埠。如果你寫(xiě)了一個(gè)真的服務(wù)器 —— 對(duì)于有雄心的讀者來(lái)說(shuō)是一個(gè)非常有趣的額外練習(xí)怠缸! —— 那么顯然你將會(huì)對(duì)這段代碼進(jìn)行它應(yīng)得的 FP 思考。

ch11-code/stock-ticker-events.js 中钳宪,我們(通過(guò) RxJS)建立了某種連接到事件發(fā)生器對(duì)象的 observable揭北。我們調(diào)用 connectToServer() 來(lái)得到這個(gè)事件發(fā)生器,然后監(jiān)聽(tīng)名為 "stock"(向我們的報(bào)價(jià)機(jī)添加新證券)和 "stock-update"(更新證券的價(jià)格以及漲跌額度)的事件使套。最后罐呼,我們?yōu)檫@些 observable 的輸入數(shù)據(jù)定義變形規(guī)則,按需要格式化數(shù)據(jù)侦高。

ch11-code/stock-ticker.js 中嫉柴,我們?cè)?stockTickerUI 對(duì)象上將 UI(DOM 副作用)的行為定義為方法。我們還定義了各種幫助函數(shù)奉呛,包括 getElemAttr(..)计螺、stripPrefix(..) 以及其他一些。最后瞧壮,我們 subscribe(..) 兩個(gè)向我們提供格式化數(shù)據(jù)的 observable 來(lái)渲染 DOM登馒。

證券時(shí)間

讓我們看看 ch11-code/stock-ticker-events.js 中的代碼。我們將從一些基本的幫助函數(shù)開(kāi)始:

function addStockName(stock) {
    return setProp( "name", stock, stock.id );
}
function formatSign(val) {
    if (Number(val) > 0) {
        return `+${val}`;
    }
    return val;
}
function formatCurrency(val) {
    return `$${val}`;
}
function transformObservable(mapperFn,obsv){
    return obsv.map( mapperFn );
}

這些純函數(shù)理解起來(lái)應(yīng)該相當(dāng)直接咆槽〕陆危回憶一下,第四章中的 setProp(..) 在設(shè)置新屬性之前實(shí)際上克隆了對(duì)象秦忿。這行使了我們?cè)诘诹轮锌吹降脑瓌t:通過(guò)將值視為不可變的 —— 即使它們不是 —— 來(lái)避免副作用麦射。

addStockName(..) 用來(lái)向一個(gè)證券消息對(duì)象添加 name 屬性,值與它的 id 相等灯谣。name 的值稍后用作控件中可見(jiàn)的證券名稱(chēng)潜秋。

transformObservable(..) 上有一個(gè)微妙的地方需要注意:因?yàn)樵谝粋€(gè) observable 上進(jìn)行 map(..) 返回一個(gè)新的 observable,所以它看起來(lái)是純粹的胎许。但從技術(shù)上講峻呛,在它的底層,obsv 的內(nèi)部狀態(tài)被改變?yōu)檫B接到 map(..) 返回的新 observable辜窑。這種副作用沒(méi)什么大不了的钩述,而且不會(huì)損害我們代碼的可讀性,但是無(wú)論副作用在什么地方你都能發(fā)現(xiàn)它們非常重要穆碎,而不是在你遇到 bug 時(shí)被它們嚇一跳切距。

當(dāng)一個(gè)來(lái)自于 “服務(wù)器” 的證券信息被接受到時(shí),它看起來(lái)就像這樣:

{ id: "AAPL", price: 121.7, change: 0.01 }

在展示在 DOM 上之前惨远,price 需要用 formatCurrency(..) 進(jìn)行格式化("$121.70")谜悟,而且 change 需要用 formatChange(..) 進(jìn)行格式化 ("+0.01")话肖。但我們不想改變消息對(duì)象,所以我們需要一個(gè)幫助函數(shù)來(lái)格式化數(shù)字并給我們一個(gè)新的證券對(duì)象葡幸;

function formatStockNumbers(stock) {
    var updateTuples = [
        [ "price", formatPrice( stock.price ) ],
        [ "change", formatChange( stock.change ) ]
    ];

    return reduce( function formatter(stock,[propName,val]){
        return setProp( propName, stock, val );
    } )
    ( stock )
    ( updateTuples );
}

我們創(chuàng)建了 updateTuples 數(shù)組來(lái)分別為 pricechange 保持兩個(gè)屬性名和格式化后的值的元組(就是數(shù)組)最筒。我們?cè)谶@個(gè)數(shù)組上 reduce(..)(參見(jiàn)第八章),將 stock 對(duì)象作為 initialValue蔚叨。我們將元組分解為 propNameval床蜘,之后返回 setProp(..) 調(diào)用,它繼而返回一個(gè)帶有設(shè)定好屬性的新的克隆對(duì)象蔑水。

現(xiàn)在讓我們?cè)俣x一些幫助函數(shù):

var formatDecimal = unboundMethod( "toFixed" )( 2 );
var formatPrice = pipe( formatDecimal, formatCurrency );
var formatChange = pipe( formatDecimal, formatSign );
var processNewStock = pipe( addStockName, formatStockNumbers );

函數(shù) formatDecimal(..) 接收一個(gè)數(shù)字(比如 2.1)并調(diào)用它的 toFixed( 2 ) 方法邢锯。我們使用第八章的 unboundMethod(..) 來(lái)建立一個(gè)獨(dú)立的推遲綁定的方法。

formatPrice(..)搀别、formatChange(..)丹擎、和 processNewStock(..) 都使用 pipe(..) 將一些操作從左至右地組合起來(lái)(參見(jiàn)第四章)。

為了從我們的事件發(fā)生器中創(chuàng)建 observable(參見(jiàn)第十章)歇父,我們需要一個(gè)幫助函數(shù)蒂培,它是 RxJS 中 Rx.Observable.fromEvent(..) 的一個(gè)柯里化(參見(jiàn)第三章)獨(dú)立函數(shù):

var makeObservableFromEvent = curry( Rx.Observable.fromEvent, 2 )( server );

這個(gè)函數(shù)被指定為監(jiān)聽(tīng) server(事件發(fā)生器),而且在等待一個(gè)事件名稱(chēng)字符串來(lái)生成它的 observable“裆唬現(xiàn)在我們準(zhǔn)備好了為兩個(gè)事件創(chuàng)建 observer 所需的所有配件护戳,可以對(duì)這些 observer 進(jìn)行映射變形來(lái)格式化輸入數(shù)據(jù)了:

var observableMapperFns = [ processNewStock, formatStockNumbers ];

var [ newStocks, stockUpdates ] = pipe(
    map( makeObservableFromEvent ),
    curry( zip )( observableMapperFns ),
    map( spreadArgs( transformObservable ) )
)
( [ "stock", "stock-update" ] );

我們從事件名稱(chēng)的數(shù)組開(kāi)始 (["stock","stock-update"]),將這個(gè)列表 map(..)(參見(jiàn)第八章)為一個(gè)包含兩個(gè) observable 的列表垂睬,然后將這個(gè)列表 zip(..)(參見(jiàn)第八章)為一個(gè) observable 映射函數(shù)的列表媳荒;這個(gè)映射生成了一個(gè)元組的列表,就像 [ observable, mapperFn ]驹饺。最后钳枕,我們使用 transformObservable(..) 來(lái) map(..) 這個(gè)元組的列表,并使用 spreadArgs(..)(參見(jiàn)第三章)來(lái)把每個(gè)元組分散為獨(dú)立的參數(shù)逻淌。

結(jié)果就是一個(gè)格式化后的 observable 列表么伯,我們通過(guò)數(shù)組解構(gòu)將它們分別賦值給 newStocksstockUpdates疟暖。

就是這樣卡儒;這就是我們?nèi)绾问褂幂p量 FP 方式建立證券報(bào)價(jià)事件 observable!我們將在 ch11-code/stock-ticker.js 中訂閱這兩個(gè) observable俐巴。

退后一步并反思一下我們?cè)谶@里對(duì) FP 原理的使用骨望。它合理嗎?你能看出我們是如何應(yīng)用這本書(shū)前幾章中講解的各種概念的嗎欣舵?你能想出完成這些任務(wù)的其他方法嗎擎鸠?

更重要的是,你如何用指令式方式完成它缘圈,而且你對(duì)這兩種方式大體上比較起來(lái)有什么看法劣光?試著練習(xí)一下袜蚕。使用你熟知的指令式方式編寫(xiě)它的等價(jià)物。如果你像我一樣绢涡,指令式形式將依然使人感覺(jué)更自然牲剃。

在繼續(xù)之前你需要 學(xué)會(huì) 的是,你 可以理解并推理我們剛剛展示的 FP 風(fēng)格雄可≡涓担考慮一下每一個(gè)函數(shù)和代碼段的形狀(輸入與輸出)。你能看出它們是如何聯(lián)系在一起的嗎数苫?

在你適應(yīng)這些東西之前聪舒,要不斷練習(xí)。

證券報(bào)價(jià)機(jī)的 UI

如果你對(duì)前一節(jié)中的 FP 感到相當(dāng)舒適虐急,那么你就準(zhǔn)備好深入 ch11-code/stock-ticker.js 了箱残。它相當(dāng)復(fù)雜,所以我們將花一些時(shí)間來(lái)看看它整體的每一個(gè)部分戏仓。

讓我們先定義一些可以輔助我們 DOM 操作任務(wù)的幫助函數(shù):

function isTextNode(node) {
    return node && node.nodeType == 3;
}
function getElemAttr(elem,prop) {
    return elem.getAttribute( prop );
}
function setElemAttr(elem,prop,val) {
    // !!副作用!!
    return elem.setAttribute( prop, val );
}
function matchingStockId(id) {
    return function isStock(node){
        return getStockId( node ) == id;
    };
}
function isStockInfoChildElem(elem) {
    return /\bstock-/i.test( getClassName( elem ) );
}
function appendDOMChild(parentNode,childNode) {
    // !!副作用!!
    parentNode.appendChild( childNode );
    return parentNode;
}
function setDOMContent(elem,html) {
    // !!副作用!!
    elem.innerHTML = html;
    return elem;
}

var createElement = document.createElement.bind( document );

var getElemAttrByName = curry( reverseArgs( getElemAttr ), 2 );
var getStockId = getElemAttrByName( "data-stock-id" );
var getClassName = getElemAttrByName( "class" );

這些東西幾乎都是自解釋的疚宇。我為 getElemAttrByName(..) 使用了 curry(reverseArgs( .. ))(參見(jiàn)第三章)而非 partialRight(..),僅僅是為了這種特定的情況擠出好一些的性能赏殃。

注意我明確指出了改變 DOM 元素狀態(tài)的副作用敷待。我們無(wú)法很容易地克隆一個(gè) DOM 對(duì)象并替換它,所以在這里我們安于一些改變既存 DOM 元素的副作用仁热。至少榜揖,如果我們?cè)?DOM 的渲染上發(fā)生了 bug,我們可以很容易地檢索這些代碼注釋來(lái)縮小可疑代碼的范圍抗蠢。

matchingStockId(..) 展示了閉包(參見(jiàn)第二章)的用法(之一>儆础)—— 創(chuàng)建一個(gè)即使稍后在一個(gè)不同作用域中運(yùn)行時(shí)也能記住變量 id 的內(nèi)部函數(shù)(isStock(..))。

這是一些其他的雜項(xiàng)幫助函數(shù):

function stripPrefix(prefixRegex) {
    return function mapperFn(val) {
        return val.replace( prefixRegex, "" );
    };
}
function listify(listOrItem) {
    if (!Array.isArray( listOrItem )) {
        return [ listOrItem ];
    }
    return listOrItem;
}

讓我們定義一個(gè)可以幫我們?nèi)〉靡粋€(gè) DOM 元素子節(jié)點(diǎn)的幫助函數(shù):

var getDOMChildren = pipe(
    listify,
    flatMap(
        pipe(
            curry( prop )( "childNodes" ),
            Array.from
        )
    )
);

首先迅矛,我們使用 listify(..) 來(lái)確保我們有一個(gè)元素的列表(即便它只有一個(gè)元素)妨猩。回憶一下第八章的 flatMap(..)秽褒,它映射一個(gè)列表并將一個(gè)列表的列表扁平化為一個(gè)淺層列表壶硅。

我們這里的映射函數(shù)將一個(gè)元素映射為它的 childNodes 列表,然后我們使用 Array.from(..) 將它變成真正的數(shù)組(而不是一個(gè)實(shí)時(shí)的 NodeList)销斟。這兩個(gè)函數(shù)(通過(guò) pipe(..))被組合為一個(gè)單獨(dú)的映射函數(shù)庐椒,這就是融合(參見(jiàn)第八章)。

現(xiàn)在蚂踊,讓我們使用這個(gè) getDOMChildren(..) 幫助函數(shù)來(lái)定義從控件中取得制定 DOM 元素的工具:

function getStockElem(tickerElem,stockId) {
    return pipe(
        getDOMChildren,
        filterOut( isTextNode ),
        filterIn( matchingStockId( stockId ) )
    )
    ( tickerElem );
}
function getStockInfoChildElems(stockElem) {
    return pipe(
        getDOMChildren,
        filterOut( isTextNode ),
        filterIn( isStockInfoChildElem )
    )
    ( stockElem );
}

getStockElem(..) 從我們控件的 tickerElem DOM 元素開(kāi)始约谈,取得它的子元素,然后過(guò)濾它來(lái)確保我們得到匹配指定證券標(biāo)識(shí)符的元素。getStockInfoChildElems(..) 做的幾乎是相同的事情棱诱,除了它是從一個(gè)證券元素開(kāi)始泼橘,而且使用不同的過(guò)濾器來(lái)過(guò)濾。

兩個(gè)工具都濾除了文本節(jié)點(diǎn)(因?yàn)樗鼈兣c真正的 DOM 節(jié)點(diǎn)的工作方式不同)迈勋,而且兩個(gè)工具都返回一個(gè) DOM 元素的數(shù)組侥加,即使它僅含有一個(gè)元素。

主 API

我們將使用一個(gè) stockTickerUI 對(duì)象來(lái)組織我們的三個(gè)主 UI 操作方法粪躬,就像這樣:

var stockTickerUI = {

    updateStockElems(stockInfoChildElemList,data) {
        // ..
    },

    updateStock(tickerElem,data) {
        // ..
    },

    addStock(tickerElem,data) {
        // ..
    }
};

讓我們首先檢視一下 updateStock(..)担败,因?yàn)樗侨齻€(gè)中最簡(jiǎn)單的:

var stockTickerUI = {

    // ..

    updateStock(tickerElem,data) {
        var getStockElemFromId = curry( getStockElem )( tickerElem );
        var stockInfoChildElemList = pipe(
            getStockElemFromId,
            getStockInfoChildElems
        )
        ( data.id );

        return stockTickerUI.updateStockElems(
            stockInfoChildElemList,
            data
        );
    },

    // ..

};

使用 tickerElem 柯里化早先的幫助函數(shù) getStockElem(..) 給了我們 getStockElemFromId(..),它將接收 data.id镰官。這個(gè) <li> 元素(實(shí)際上提前,是這個(gè)元素的列表)被傳遞給 getStockInfoChildElems(..),給了我們?nèi)齻€(gè) <span> 子元素來(lái)展示證券信息泳唠,我們稱(chēng)之為 stockInfoChildElemList狈网。我們將這個(gè)列表與證券 data 消息對(duì)象一起傳遞給 stockTickerUI.updateStockElems(..) 來(lái)真正地使用更新過(guò)的數(shù)據(jù)更新那些 <span>

現(xiàn)在讓我們看看 stockTickerUI.updateStockElems(..)

var stockTickerUI = {

    updateStockElems(stockInfoChildElemList,data) {
        var getDataVal = curry( reverseArgs( prop ), 2 )( data );
        var extractInfoChildElemVal = pipe(
            getClassName,
            stripPrefix( /\bstock-/i ),
            getDataVal
        );
        var orderedDataVals =
            map( extractInfoChildElemVal )( stockInfoChildElemList );
        var elemsValsTuples =
            filterOut( function updateValueMissing([infoChildElem,val]){
                return val === undefined;
            } )
            ( zip( stockInfoChildElemList, orderedDataVals ) );

        // !!副作用!!
        compose( each, spreadArgs )
        ( setDOMContent )
        ( elemsValsTuples );
    },

    // ..

};

我知道笨腥,信息量有點(diǎn)兒大拓哺。我們一個(gè)語(yǔ)句一個(gè)語(yǔ)句地分解它。

getDataVal(..) 被反轉(zhuǎn)參數(shù)順序后柯里化脖母,再綁定掉 data 消息對(duì)象上士鸥,現(xiàn)在它在等待一個(gè)屬性名以便從 data 中進(jìn)行抽取。

接下來(lái)讓我們看看 extractInfoChildElem

var extractInfoChildElemVal = pipe(
    getClassName,
    stripPrefix( /\bstock-/i ),
    getDataVal
);

這個(gè)函數(shù)接收一個(gè) DOM 元素谆级,取得它的 DOM class烤礁,截去 "stock-" 前綴,然后使用這個(gè)值("name"肥照、"price"脚仔、或 "change")通過(guò) getDataVal(..)data 中抽取同名屬性的值。表面上看舆绎,這種行為可能有些奇怪鲤脏。

它的目的是按照與 <span> 元素(在 stockInfoChildElemList 中)相同的順序從 data 中抽取值。我們通過(guò)將 extractInfoChildElem(..) 用作這個(gè)列表的映射函數(shù)來(lái)完成這個(gè)任務(wù)吕朵,將其結(jié)果列表稱(chēng)為 orderedDataVals猎醇。

下面,我們將把 <span> 列表和值的列表 zip 起來(lái)边锁,生成一些元組:

zip( stockInfoChildElemList, orderedDataVals )

由于我們定義 observable 變形的方式姑食,這里有一個(gè)微妙有趣的小問(wèn)題波岛,新的證券消息對(duì)象在 data 中有一個(gè) name 屬性可以與 <span class="stock-name"> 元素相匹配茅坛,但是在更新用的消息對(duì)象上 name 就不存在。

作為一個(gè)一般的概念,如果數(shù)據(jù)消息對(duì)象沒(méi)有一個(gè)屬性贡蓖,我們就不應(yīng)該更新相應(yīng)的 DOM 元素曹鸠。所以,我們需要 filterOut(..) 所有值(在這個(gè)例子中斥铺,是第二個(gè)位置)為 undefined 的元組:

var elemsValsTuples =
    filterOut( function updateValueMissing([infoChildElem,val]){
        return val === undefined;
    } )
    ( zip( stockInfoChildElemList, orderedDataVals ) );

這個(gè)過(guò)濾處理的結(jié)果是一個(gè)準(zhǔn)備用于更新 DOM 內(nèi)容的元組列表(就像 [ <span>, ".." ])彻桃,我們將它賦值給 elemsValsTuples

注意: 因?yàn)榕卸ê瘮?shù) updateValueMissing(..) 在這里被內(nèi)聯(lián)地指定晾蜘,所以我們可以控制它的簽名邻眷。與使用 spreadArgs(..) 來(lái)適配它,以便將一個(gè)數(shù)組的實(shí)際參數(shù)擴(kuò)散為兩個(gè)單獨(dú)的命名形式參數(shù)不同剔交,我們?cè)诤瘮?shù)聲明中使用了形式參數(shù)數(shù)組解構(gòu)(function updateValueMissing([infoChildElem,val]){ ..)肆饶;更多信息可以參見(jiàn)第二章。

最后岖常,我們需要更新 <span> 元素的 DOM 內(nèi)容:

// !!副作用!!
compose( each, spreadArgs )( setDOMContent )
( elemsValsTuples );

我們使用 each(..)(參見(jiàn)第八章中 forEach(..) 的討論)迭代這個(gè) elemsValsTuples 列表驯镊。

與其他地方使用的 pipe(..) 不同,這個(gè)組合使用 compose(..)(參見(jiàn)第四章)來(lái)將 setDomContent(..) 傳入 spreadArgs(..)竭鞍,然后其結(jié)果作為迭代函數(shù)被傳遞給 each(..)板惑。每一個(gè)元組都被擴(kuò)散為 setDOMContent(..) 的參數(shù),之后由它更新相應(yīng)的 DOM 元素偎快。

解決了兩個(gè)主 UI 方法冯乘,還有一個(gè):addStock(..)。我們先整體地定義它晒夹,然后像之前一樣一步一步地檢視它:

var stockTickerUI = {

    // ..

    addStock(tickerElem,data) {
        var [stockElem, ...infoChildElems] = map(
            createElement
        )
        ( [ "li", "span", "span", "span" ] );
        var attrValTuples = [
            [ ["class","stock"], ["data-stock-id",data.id] ],
            [ ["class","stock-name"] ],
            [ ["class","stock-price"] ],
            [ ["class","stock-change"] ]
        ];
        var elemsAttrsTuples =
            zip( [stockElem, ...infoChildElems], attrValTuples );

        // !!副作用!!
        each( function setElemAttrs([elem,attrValTupleList]){
            each(
                spreadArgs( partial( setElemAttr, elem ) )
            )
            ( attrValTupleList );
        } )
        ( elemsAttrsTuples );

        // !!副作用!!
        stockTickerUI.updateStockElems( infoChildElems, data );
        reduce( appendDOMChild )( stockElem )( infoChildElems );
        tickerElem.appendChild( stockElem );
    }

};

這個(gè) UI 方法需要為新的證券元素創(chuàng)建空的 DOM 結(jié)構(gòu)往湿,之后使用 stockTickerUI.updateStockElems(..) 像之前描述過(guò)的那樣更新它的內(nèi)容。

首先:

var [stockElem, ...infoChildElems] = map(
    createElement
)
( [ "li", "span", "span", "span" ] );

我們創(chuàng)建父節(jié)點(diǎn) <li> 和三個(gè)子節(jié)點(diǎn)<span> 元素惋戏,將它們分別賦值給 stockEleminfoChildElems 列表领追。

為了使用恰當(dāng)?shù)?DOM 屬性來(lái)初始化這些元素,我們創(chuàng)建了一個(gè)元組列表的列表响逢。每一個(gè)主列表中的項(xiàng)目都按順序代表那四個(gè)元素绒窑。在子列表中的每一個(gè)元組都代表一個(gè)要被設(shè)置到相應(yīng) DOM 元素上的屬性-值對(duì):

var attrValTuples = [
    [ ["class","stock"], ["data-stock-id",data.id] ],
    [ ["class","stock-name"] ],
    [ ["class","stock-price"] ],
    [ ["class","stock-change"] ]
];

現(xiàn)在我們要將四個(gè)元素的列表與這個(gè) attrValTuples 列表 zip(..) 起來(lái):

var elemsAttrsTuples =
    zip( [stockElem, ...infoChildElems], attrValTuples );

最后列表的結(jié)構(gòu)看起來(lái)將是這樣:

[
    [ <li>, [ ["class","stock"], ["data-stock-id",data.id] ] ],
    [ <span>, [ ["class","stock-name"] ] ],
    ..
]

如果我們想要指令式地處理這種數(shù)據(jù)結(jié)構(gòu),將屬性-值元組賦值到每個(gè) DOM 元素的話(huà)舔亭,我們可能要使用嵌套的 for 循環(huán)些膨。我們的 FP 方式也類(lèi)似,不過(guò)使用的是嵌套的 each(..) 迭代:

// !!副作用!!
each( function setElemAttrs([elem,attrValTupleList]){
    each(
        spreadArgs( partial( setElemAttr, elem ) )
    )
    ( attrValTupleList );
} )
( elemsAttrsTuples );

外側(cè)的 each(..) 迭代元組的列表钦铺,同時(shí)將每個(gè) elem 以及與之相關(guān)聯(lián)的 attrValTupleList 通過(guò)前面講過(guò)的形式參數(shù)數(shù)組解構(gòu)擴(kuò)散到 setElemAttrs(..) 的命名形式參數(shù)上订雾。

在這個(gè)外側(cè)迭代 “循環(huán)” 內(nèi)部,使用一個(gè)內(nèi)側(cè)的 each(..) 迭代屬性-值元組的子列表矛洞。內(nèi)側(cè)的迭代函數(shù)是使用 elem 作為第一個(gè)參數(shù)對(duì) setElemAttr(..) 進(jìn)行局部應(yīng)用洼哎,然后對(duì)其進(jìn)行參數(shù)擴(kuò)散烫映。

到這里,我們有了一個(gè) <span> 元素的列表噩峦,每一個(gè)都被屬性填充好了锭沟,但是還沒(méi)有 innerHTML 內(nèi)容。我們使用 stockTickerUI.updateStockElems(..)data 設(shè)置到 <span> 子元素识补,和證券更新事件一樣族淮。

現(xiàn)在,我們需要將這些 <span> 追加到父節(jié)點(diǎn) <li> 上去凭涂,而且我們使用 reduce(..)(參見(jiàn)第八章)來(lái)這樣做:

reduce( appendDOMChild )( stockElem )( infoChildElems );

最后祝辣,用一個(gè)老式的 DOM 變更副作用將新的證券元組追加到控件的 DOM 上:

tickerElem.appendChild( stockElem );

咻!你都跟上了切油?在前進(jìn)之前较幌,我建議你回過(guò)頭去將這次討論重讀幾分鐘并實(shí)踐一下代碼。

訂閱 Observable

我們最后的主要任務(wù)是訂閱定義在 ch11-code/stock-ticker-events.js 中的 observable白翻,并將這些訂閱內(nèi)容連接在恰當(dāng)?shù)闹?UI 方法(addStock(..)updateStock(..))上乍炉。

首先,我們注意到這些方法每個(gè)都期待 tickerElem 作為第一個(gè)參數(shù)滤馍。我們來(lái)制造一個(gè)列表(stockTickerUIMethodsWithDOMContext)岛琼,它通過(guò)局部應(yīng)用(也就是閉包;參見(jiàn)第二章)將 ticker 控件的 DOM 元素與這兩個(gè)方法封裝起來(lái):

var ticker = document.getElementById( "stock-ticker" );

var stockTickerUIMethodsWithDOMContext = map(
    curry( reverseArgs( partial ), 2 )( ticker )
)
( [ stockTickerUI.addStock, stockTickerUI.updateStock ] );

和之前一樣巢株,reverseArgs( partial )partialRight(..) 性能優(yōu)化后的替代品槐瑞。但是這次,partial(..) 是我們要使用的映射函數(shù)阁苞。為此困檩,我們需要 curry(..) 它以便提前制定第二個(gè)參數(shù) ticker;當(dāng)每個(gè) UI 方法之后被映射時(shí)那槽,它會(huì)使用 ticker 對(duì)這個(gè)函數(shù)進(jìn)行局部應(yīng)用〉垦兀現(xiàn)在,這兩個(gè)在結(jié)果數(shù)組中的雙重局部應(yīng)用函數(shù)可以用于訂閱 observable 了骚灸。

雖然我們使用了閉包來(lái)將 ticker 的狀態(tài)保留在這兩個(gè)函數(shù)中糟趾,但是在第七章中我們看到了可以將這個(gè) ticker 值作為一個(gè)對(duì)象上的屬性 “保留” 下來(lái),也許是通過(guò)用 this 將每個(gè)函數(shù)綁定到 stockTickerUI甚牲。因?yàn)?this 是一種隱含的輸入(參見(jiàn)第二章)义郑,而且這通常來(lái)說(shuō)不太好,所以我選擇了閉包而非對(duì)象丈钙。

為了訂閱 observable非驮,我們來(lái)制造一個(gè)用于解綁定方法的幫助函數(shù):

var subscribeToObservable =
    pipe( uncurry, spreadArgs )( unboundMethod( "subscribe" ) );

unboundMethod("subscribe") 是自動(dòng)被柯里化的,所以我們 uncurry(..) 它(參見(jiàn)第三章)雏赦,然后使用 spreadArgs(..) 適配它(同樣參照第三章)劫笙,這樣它將把一個(gè)元組數(shù)組擴(kuò)散為它的兩個(gè)參數(shù)芙扎。

現(xiàn)在,我們只需要一個(gè) observable 的列表邀摆,這樣我們就可以將它與封裝了上下文環(huán)境的 UI 方法列表 zip(..) 在一起。之后這個(gè)元組的列表的每一個(gè)都可以使用我們剛剛在前一個(gè)代碼段中定義的 subscribeToObservable(..) 進(jìn)行訂閱:

var stockTickerObservables = [ newStocks, stockUpdates ];

// !!副作用!!
each( subscribeToObservable )
( zip( stockTickerUIMethodsWithDOMContext, stockTickerObservables ) );

因?yàn)閺募夹g(shù)上講伍茄,為了訂閱這些 observable栋盹,我們?cè)诟淖兯鼈兊臓顟B(tài),而且也因?yàn)槲覀冊(cè)谑褂?each(..) —— 幾乎總是與副作用相關(guān)敷矫! —— 所以我們?cè)诖a注釋中指出這個(gè)事實(shí)例获。

就是這樣!就像早先我們?cè)谟懻撟C券報(bào)價(jià)機(jī)事件時(shí)做的那樣曹仗,花同樣的時(shí)間重復(fù)閱讀并將這段代碼與它的指令式形式進(jìn)行比較吧榨汤。真的,花些時(shí)間來(lái)做怎茫。我知道這是一本很厚的書(shū)收壕,但你讀了這么多說(shuō)到底是為了能夠消化并理解這樣的代碼。

你對(duì)在 JavaScript 中以一種平衡的方式使用 FP 感覺(jué)怎么樣轨蛤?就像我們?cè)谶@里做的一樣蜜宪,繼續(xù)練習(xí)吧!

總結(jié)

我們?cè)诒菊轮杏懻摰拇a應(yīng)當(dāng)作為整體來(lái)看待祥山,而不只是像在本章中一樣被打碎成片段圃验。如果還沒(méi)這么做過(guò)的話(huà),現(xiàn)在就停下去通讀完整的文件吧缝呕。確保你能在完整的上下文中理解它們澳窑。

這個(gè)代碼示例不是要成為你應(yīng)當(dāng)如何確切編寫(xiě)代碼的規(guī)定。它是想要更好地描述如何使用輕量 FP 技術(shù)來(lái)思考并著手處理這樣的任務(wù)供常。它想要盡可能多地將本書(shū)中的不同概念關(guān)聯(lián)起來(lái)摊聋。它想要在更 “真實(shí)” 的代碼場(chǎng)景下探索 FP,而非我們通常引用的代碼片段栈暇。

我十分確信我在自己的旅程中更好地理解了 FP栗精,我將會(huì)繼續(xù)改進(jìn)自己編寫(xiě)這段實(shí)例代碼的方式。你現(xiàn)在看到的只是我的弧線(xiàn)上的一個(gè)快照瞻鹏。我希望這對(duì)你也一樣悲立。

在我們結(jié)束本書(shū)正文之前,我想提醒你的是我在第一章中分享過(guò)的這幅可讀性曲線(xiàn):

在這次學(xué)習(xí)并在你的 JavaScript 中應(yīng)用 FP 的旅途中新博,將這幅圖牢記在心并為你自己設(shè)定現(xiàn)實(shí)的目標(biāo)是十分重要的薪夕。你已經(jīng)堅(jiān)持到了這里,這是十分了不起的成就赫悄。

但是當(dāng)你向著絕望和失落的低谷傾斜的時(shí)候不要放棄原献。在另一邊等著你的是一種對(duì)你代碼的思考和交流的方式馏慨,它更易于解讀,易于理解姑隅,易于驗(yàn)證写隶,而最終,更加可靠讲仰。

對(duì)于我們開(kāi)發(fā)者來(lái)說(shuō)慕趴,我想不出值得為之努力的更高尚的目標(biāo)。感謝你與我分享在 JavaScript 中學(xué)習(xí) FP 原理的旅途鄙陡。希望你的旅途和我的一樣豐富多彩冕房!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市趁矾,隨后出現(xiàn)的幾起案子耙册,更是在濱河造成了極大的恐慌,老刑警劉巖毫捣,帶你破解...
    沈念sama閱讀 221,695評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件详拙,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡蔓同,警方通過(guò)查閱死者的電腦和手機(jī)溪厘,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,569評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)牌柄,“玉大人畸悬,你說(shuō)我怎么就攤上這事∩河叮” “怎么了蹋宦?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,130評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)咒锻。 經(jīng)常有香客問(wèn)我冷冗,道長(zhǎng),這世上最難降的妖魔是什么惑艇? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,648評(píng)論 1 297
  • 正文 為了忘掉前任蒿辙,我火速辦了婚禮,結(jié)果婚禮上滨巴,老公的妹妹穿的比我還像新娘思灌。我一直安慰自己,他們只是感情好恭取,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,655評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布泰偿。 她就那樣靜靜地躺著,像睡著了一般蜈垮。 火紅的嫁衣襯著肌膚如雪耗跛。 梳的紋絲不亂的頭發(fā)上裕照,一...
    開(kāi)封第一講書(shū)人閱讀 52,268評(píng)論 1 309
  • 那天,我揣著相機(jī)與錄音调塌,去河邊找鬼晋南。 笑死,一個(gè)胖子當(dāng)著我的面吹牛羔砾,可吹牛的內(nèi)容都是我干的负间。 我是一名探鬼主播,決...
    沈念sama閱讀 40,835評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼蜒茄,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼唉擂!你這毒婦竟也來(lái)了餐屎?” 一聲冷哼從身側(cè)響起檀葛,我...
    開(kāi)封第一講書(shū)人閱讀 39,740評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤查刻,失蹤者是張志新(化名)和其女友劉穎虾宇,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體臭墨,經(jīng)...
    沈念sama閱讀 46,286評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡藏鹊,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,375評(píng)論 3 340
  • 正文 我和宋清朗相戀三年润讥,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片盘寡。...
    茶點(diǎn)故事閱讀 40,505評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡楚殿,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出竿痰,到底是詐尸還是另有隱情脆粥,我是刑警寧澤,帶...
    沈念sama閱讀 36,185評(píng)論 5 350
  • 正文 年R本政府宣布影涉,位于F島的核電站变隔,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏蟹倾。R本人自食惡果不足惜匣缘,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,873評(píng)論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望鲜棠。 院中可真熱鬧肌厨,春花似錦、人聲如沸豁陆。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,357評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)献联。三九已至竖配,卻和暖如春何址,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背进胯。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,466評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工用爪, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人胁镐。 一個(gè)月前我還...
    沈念sama閱讀 48,921評(píng)論 3 376
  • 正文 我出身青樓偎血,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親盯漂。 傳聞我的和親對(duì)象是個(gè)殘疾皇子颇玷,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,515評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容