感謝社區(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)分別為 price
和 change
保持兩個(gè)屬性名和格式化后的值的元組(就是數(shù)組)最筒。我們?cè)谶@個(gè)數(shù)組上 reduce(..)
(參見(jiàn)第八章),將 stock
對(duì)象作為 initialValue
蔚叨。我們將元組分解為 propName
和 val
床蜘,之后返回 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)將它們分別賦值給 newStocks
和 stockUpdates
疟暖。
就是這樣卡儒;這就是我們?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>
元素惋戏,將它們分別賦值給 stockElem
和 infoChildElems
列表领追。
為了使用恰當(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 原理的旅途鄙陡。希望你的旅途和我的一樣豐富多彩冕房!