感謝社區(qū)中各位的大力支持,譯者再次奉上一點點福利:阿里云產(chǎn)品券,享受所有官網(wǎng)優(yōu)惠,并抽取幸運大獎:點擊這里領(lǐng)取
現(xiàn)在,你擁有了為了理解輕量函數(shù)式 JavaScript 所需的一切。再沒有新的概念要介紹了。
在這最后的一章中,我們的目標是凝聚這些概念。我們將看到將這本書中的許多主題融合在一起的代碼 —— 應用我們學到的東西。最重要的是,這篇代碼示例是為了展示 “輕量函數(shù)式” 應用到 JavaScript 上的方式 —— 也就是,平衡以及教條之上的實用主義。
你將會想要廣泛地親自實踐這些技術(shù)。消化理解這一章對于你將 FP 的概念應用于真實世界的代碼至關(guān)重要。
準備
讓我們建造一個簡單的證券報價機控件。
注意: 為了便于引用,這個示例的全部代碼位于 ch11-code/ 子目錄中 —— 參見這本書的 GitHub 代碼庫 (https://github.com/getify/Functional-Light-JS)。另外,這個示例需要選用一些我們在這本書中討論過的 FP 幫助函數(shù),它們包含在 ch11-code/fp-helpers.js 中。本章中我們僅將注意力集中在與我們的討論有關(guān)的部分代碼中。
首先,讓我談談這個控件的置標代碼,這樣我們才有地方來展示信息。我們從 ch11-code/index.html 文件中的一個空 <ul ..> 元素開始,當運行的時候,DOM 將會被填充為:
<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>
在我們前進之前,讓我提醒你一下:與 DOM 的交互是一種 I/O,而這意味著副作用。我們不能消滅這些副作用,但可以限制并控制它們。我們要確實有意地將我們的應用程序處理 DOM 的表面積控制在最小。我們已經(jīng)在第五章中學習過這些技術(shù)了。
概括一下我們控件的功能:這段代碼將會在每次 “收到” 新證券事件時添加一個 <li ..> 元素,并在證券更新事件到來時更新它的價格。
在第十一章的示例代碼,ch11-code/mock-server.js 中,我們設(shè)立了某種定時器,它隨機地向一個簡單事件發(fā)生器推送虛構(gòu)的證券數(shù)據(jù),來模擬我們正在從一個服務器接受證券信息。我們暴露了一個 connectToServer() 函數(shù)假裝這樣做,但實際上只是一個虛構(gòu)的事件發(fā)生器實例。
注意: 這個文件中都是虛構(gòu)/模擬的行為,所以我沒有花費太多的力氣來使它支持 FP。我不建議你花太多的時間來關(guān)心這個文件中的代碼。如果你寫了一個真的服務器 —— 對于有雄心的讀者來說是一個非常有趣的額外練習! —— 那么顯然你將會對這段代碼進行它應得的 FP 思考。
在 ch11-code/stock-ticker-events.js 中,我們(通過 RxJS)建立了某種連接到事件發(fā)生器對象的 observable。我們調(diào)用 connectToServer() 來得到這個事件發(fā)生器,然后監(jiān)聽名為 "stock"(向我們的報價機添加新證券)和 "stock-update"(更新證券的價格以及漲跌額度)的事件。最后,我們?yōu)檫@些 observable 的輸入數(shù)據(jù)定義變形規(guī)則,按需要格式化數(shù)據(jù)。
在 ch11-code/stock-ticker.js 中,我們在 stockTickerUI 對象上將 UI(DOM 副作用)的行為定義為方法。我們還定義了各種幫助函數(shù),包括 getElemAttr(..)、stripPrefix(..) 以及其他一些。最后,我們 subscribe(..) 兩個向我們提供格式化數(shù)據(jù)的 observable 來渲染 DOM。
證券時間
讓我們看看 ch11-code/stock-ticker-events.js 中的代碼。我們將從一些基本的幫助函數(shù)開始:
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ù)理解起來應該相當直接?;貞浺幌拢谒恼轮械?setProp(..) 在設(shè)置新屬性之前實際上克隆了對象。這行使了我們在第六章中看到的原則:通過將值視為不可變的 —— 即使它們不是 —— 來避免副作用。
addStockName(..) 用來向一個證券消息對象添加 name 屬性,值與它的 id 相等。name 的值稍后用作控件中可見的證券名稱。
在 transformObservable(..) 上有一個微妙的地方需要注意:因為在一個 observable 上進行 map(..) 返回一個新的 observable,所以它看起來是純粹的。但從技術(shù)上講,在它的底層,obsv 的內(nèi)部狀態(tài)被改變?yōu)檫B接到 map(..) 返回的新 observable。這種副作用沒什么大不了的,而且不會損害我們代碼的可讀性,但是無論副作用在什么地方你都能發(fā)現(xiàn)它們非常重要,而不是在你遇到 bug 時被它們嚇一跳。
當一個來自于 “服務器” 的證券信息被接受到時,它看起來就像這樣:
{ id: "AAPL", price: 121.7, change: 0.01 }
在展示在 DOM 上之前,price 需要用 formatCurrency(..) 進行格式化("$121.70"),而且 change 需要用 formatChange(..) 進行格式化 ("+0.01")。但我們不想改變消息對象,所以我們需要一個幫助函數(shù)來格式化數(shù)字并給我們一個新的證券對象;
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ù)組來分別為 price 和 change 保持兩個屬性名和格式化后的值的元組(就是數(shù)組)。我們在這個數(shù)組上 reduce(..)(參見第八章),將 stock 對象作為 initialValue。我們將元組分解為 propName 和 val,之后返回 setProp(..) 調(diào)用,它繼而返回一個帶有設(shè)定好屬性的新的克隆對象。
現(xiàn)在讓我們再定義一些幫助函數(shù):
var formatDecimal = unboundMethod( "toFixed" )( 2 );
var formatPrice = pipe( formatDecimal, formatCurrency );
var formatChange = pipe( formatDecimal, formatSign );
var processNewStock = pipe( addStockName, formatStockNumbers );
函數(shù) formatDecimal(..) 接收一個數(shù)字(比如 2.1)并調(diào)用它的 toFixed( 2 ) 方法。我們使用第八章的 unboundMethod(..) 來建立一個獨立的推遲綁定的方法。
formatPrice(..)、formatChange(..)、和 processNewStock(..) 都使用 pipe(..) 將一些操作從左至右地組合起來(參見第四章)。
為了從我們的事件發(fā)生器中創(chuàng)建 observable(參見第十章),我們需要一個幫助函數(shù),它是 RxJS 中 Rx.Observable.fromEvent(..) 的一個柯里化(參見第三章)獨立函數(shù):
var makeObservableFromEvent = curry( Rx.Observable.fromEvent, 2 )( server );
這個函數(shù)被指定為監(jiān)聽 server(事件發(fā)生器),而且在等待一個事件名稱字符串來生成它的 observable?,F(xiàn)在我們準備好了為兩個事件創(chuàng)建 observer 所需的所有配件,可以對這些 observer 進行映射變形來格式化輸入數(shù)據(jù)了:
var observableMapperFns = [ processNewStock, formatStockNumbers ];
var [ newStocks, stockUpdates ] = pipe(
map( makeObservableFromEvent ),
curry( zip )( observableMapperFns ),
map( spreadArgs( transformObservable ) )
)
( [ "stock", "stock-update" ] );
我們從事件名稱的數(shù)組開始 (["stock","stock-update"]),將這個列表 map(..)(參見第八章)為一個包含兩個 observable 的列表,然后將這個列表 zip(..)(參見第八章)為一個 observable 映射函數(shù)的列表;這個映射生成了一個元組的列表,就像 [ observable, mapperFn ]。最后,我們使用 transformObservable(..) 來 map(..) 這個元組的列表,并使用 spreadArgs(..)(參見第三章)來把每個元組分散為獨立的參數(shù)。
結(jié)果就是一個格式化后的 observable 列表,我們通過數(shù)組解構(gòu)將它們分別賦值給 newStocks 和 stockUpdates。
就是這樣;這就是我們?nèi)绾问褂幂p量 FP 方式建立證券報價事件 observable!我們將在 ch11-code/stock-ticker.js 中訂閱這兩個 observable。
退后一步并反思一下我們在這里對 FP 原理的使用。它合理嗎?你能看出我們是如何應用這本書前幾章中講解的各種概念的嗎?你能想出完成這些任務的其他方法嗎?
更重要的是,你如何用指令式方式完成它,而且你對這兩種方式大體上比較起來有什么看法?試著練習一下。使用你熟知的指令式方式編寫它的等價物。如果你像我一樣,指令式形式將依然使人感覺更自然。
在繼續(xù)之前你需要 學會 的是,你 也 可以理解并推理我們剛剛展示的 FP 風格??紤]一下每一個函數(shù)和代碼段的形狀(輸入與輸出)。你能看出它們是如何聯(lián)系在一起的嗎?
在你適應這些東西之前,要不斷練習。
證券報價機的 UI
如果你對前一節(jié)中的 FP 感到相當舒適,那么你就準備好深入 ch11-code/stock-ticker.js 了。它相當復雜,所以我們將花一些時間來看看它整體的每一個部分。
讓我們先定義一些可以輔助我們 DOM 操作任務的幫助函數(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( .. ))(參見第三章)而非 partialRight(..),僅僅是為了這種特定的情況擠出好一些的性能。
注意我明確指出了改變 DOM 元素狀態(tài)的副作用。我們無法很容易地克隆一個 DOM 對象并替換它,所以在這里我們安于一些改變既存 DOM 元素的副作用。至少,如果我們在 DOM 的渲染上發(fā)生了 bug,我們可以很容易地檢索這些代碼注釋來縮小可疑代碼的范圍。
matchingStockId(..) 展示了閉包(參見第二章)的用法(之一?。?創(chuàng)建一個即使稍后在一個不同作用域中運行時也能記住變量 id 的內(nèi)部函數(shù)(isStock(..))。
這是一些其他的雜項幫助函數(shù):
function stripPrefix(prefixRegex) {
return function mapperFn(val) {
return val.replace( prefixRegex, "" );
};
}
function listify(listOrItem) {
if (!Array.isArray( listOrItem )) {
return [ listOrItem ];
}
return listOrItem;
}
讓我們定義一個可以幫我們?nèi)〉靡粋€ DOM 元素子節(jié)點的幫助函數(shù):
var getDOMChildren = pipe(
listify,
flatMap(
pipe(
curry( prop )( "childNodes" ),
Array.from
)
)
);
首先,我們使用 listify(..) 來確保我們有一個元素的列表(即便它只有一個元素)?;貞浺幌碌诎苏碌?flatMap(..),它映射一個列表并將一個列表的列表扁平化為一個淺層列表。
我們這里的映射函數(shù)將一個元素映射為它的 childNodes 列表,然后我們使用 Array.from(..) 將它變成真正的數(shù)組(而不是一個實時的 NodeList)。這兩個函數(shù)(通過 pipe(..))被組合為一個單獨的映射函數(shù),這就是融合(參見第八章)。
現(xiàn)在,讓我們使用這個 getDOMChildren(..) 幫助函數(shù)來定義從控件中取得制定 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 元素開始,取得它的子元素,然后過濾它來確保我們得到匹配指定證券標識符的元素。getStockInfoChildElems(..) 做的幾乎是相同的事情,除了它是從一個證券元素開始,而且使用不同的過濾器來過濾。
兩個工具都濾除了文本節(jié)點(因為它們與真正的 DOM 節(jié)點的工作方式不同),而且兩個工具都返回一個 DOM 元素的數(shù)組,即使它僅含有一個元素。
主 API
我們將使用一個 stockTickerUI 對象來組織我們的三個主 UI 操作方法,就像這樣:
var stockTickerUI = {
updateStockElems(stockInfoChildElemList,data) {
// ..
},
updateStock(tickerElem,data) {
// ..
},
addStock(tickerElem,data) {
// ..
}
};
讓我們首先檢視一下 updateStock(..),因為它是三個中最簡單的:
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。這個 <li> 元素(實際上,是這個元素的列表)被傳遞給 getStockInfoChildElems(..),給了我們?nèi)齻€ <span> 子元素來展示證券信息,我們稱之為 stockInfoChildElemList。我們將這個列表與證券 data 消息對象一起傳遞給 stockTickerUI.updateStockElems(..) 來真正地使用更新過的數(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 );
},
// ..
};
我知道,信息量有點兒大。我們一個語句一個語句地分解它。
getDataVal(..) 被反轉(zhuǎn)參數(shù)順序后柯里化,再綁定掉 data 消息對象上,現(xiàn)在它在等待一個屬性名以便從 data 中進行抽取。
接下來讓我們看看 extractInfoChildElem:
var extractInfoChildElemVal = pipe(
getClassName,
stripPrefix( /\bstock-/i ),
getDataVal
);
這個函數(shù)接收一個 DOM 元素,取得它的 DOM class,截去 "stock-" 前綴,然后使用這個值("name"、"price"、或 "change")通過 getDataVal(..) 從 data 中抽取同名屬性的值。表面上看,這種行為可能有些奇怪。
它的目的是按照與 <span> 元素(在 stockInfoChildElemList 中)相同的順序從 data 中抽取值。我們通過將 extractInfoChildElem(..) 用作這個列表的映射函數(shù)來完成這個任務,將其結(jié)果列表稱為 orderedDataVals。
下面,我們將把 <span> 列表和值的列表 zip 起來,生成一些元組:
zip( stockInfoChildElemList, orderedDataVals )
由于我們定義 observable 變形的方式,這里有一個微妙有趣的小問題,新的證券消息對象在 data 中有一個 name 屬性可以與 <span class="stock-name"> 元素相匹配,但是在更新用的消息對象上 name 就不存在。
作為一個一般的概念,如果數(shù)據(jù)消息對象沒有一個屬性,我們就不應該更新相應的 DOM 元素。所以,我們需要 filterOut(..) 所有值(在這個例子中,是第二個位置)為 undefined 的元組:
var elemsValsTuples =
filterOut( function updateValueMissing([infoChildElem,val]){
return val === undefined;
} )
( zip( stockInfoChildElemList, orderedDataVals ) );
這個過濾處理的結(jié)果是一個準備用于更新 DOM 內(nèi)容的元組列表(就像 [ <span>, ".." ]),我們將它賦值給 elemsValsTuples。
注意: 因為判定函數(shù) updateValueMissing(..) 在這里被內(nèi)聯(lián)地指定,所以我們可以控制它的簽名。與使用 spreadArgs(..) 來適配它,以便將一個數(shù)組的實際參數(shù)擴散為兩個單獨的命名形式參數(shù)不同,我們在函數(shù)聲明中使用了形式參數(shù)數(shù)組解構(gòu)(function updateValueMissing([infoChildElem,val]){ ..);更多信息可以參見第二章。
最后,我們需要更新 <span> 元素的 DOM 內(nèi)容:
// !!副作用!!
compose( each, spreadArgs )( setDOMContent )
( elemsValsTuples );
我們使用 each(..)(參見第八章中 forEach(..) 的討論)迭代這個 elemsValsTuples 列表。
與其他地方使用的 pipe(..) 不同,這個組合使用 compose(..)(參見第四章)來將 setDomContent(..) 傳入 spreadArgs(..),然后其結(jié)果作為迭代函數(shù)被傳遞給 each(..)。每一個元組都被擴散為 setDOMContent(..) 的參數(shù),之后由它更新相應的 DOM 元素。
解決了兩個主 UI 方法,還有一個: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 );
}
};
這個 UI 方法需要為新的證券元素創(chuàng)建空的 DOM 結(jié)構(gòu),之后使用 stockTickerUI.updateStockElems(..) 像之前描述過的那樣更新它的內(nèi)容。
首先:
var [stockElem, ...infoChildElems] = map(
createElement
)
( [ "li", "span", "span", "span" ] );
我們創(chuàng)建父節(jié)點 <li> 和三個子節(jié)點<span> 元素,將它們分別賦值給 stockElem 和 infoChildElems 列表。
為了使用恰當?shù)?DOM 屬性來初始化這些元素,我們創(chuàng)建了一個元組列表的列表。每一個主列表中的項目都按順序代表那四個元素。在子列表中的每一個元組都代表一個要被設(shè)置到相應 DOM 元素上的屬性-值對:
var attrValTuples = [
[ ["class","stock"], ["data-stock-id",data.id] ],
[ ["class","stock-name"] ],
[ ["class","stock-price"] ],
[ ["class","stock-change"] ]
];
現(xiàn)在我們要將四個元素的列表與這個 attrValTuples 列表 zip(..) 起來:
var elemsAttrsTuples =
zip( [stockElem, ...infoChildElems], attrValTuples );
最后列表的結(jié)構(gòu)看起來將是這樣:
[
[ <li>, [ ["class","stock"], ["data-stock-id",data.id] ] ],
[ <span>, [ ["class","stock-name"] ] ],
..
]
如果我們想要指令式地處理這種數(shù)據(jù)結(jié)構(gòu),將屬性-值元組賦值到每個 DOM 元素的話,我們可能要使用嵌套的 for 循環(huán)。我們的 FP 方式也類似,不過使用的是嵌套的 each(..) 迭代:
// !!副作用!!
each( function setElemAttrs([elem,attrValTupleList]){
each(
spreadArgs( partial( setElemAttr, elem ) )
)
( attrValTupleList );
} )
( elemsAttrsTuples );
外側(cè)的 each(..) 迭代元組的列表,同時將每個 elem 以及與之相關(guān)聯(lián)的 attrValTupleList 通過前面講過的形式參數(shù)數(shù)組解構(gòu)擴散到 setElemAttrs(..) 的命名形式參數(shù)上。
在這個外側(cè)迭代 “循環(huán)” 內(nèi)部,使用一個內(nèi)側(cè)的 each(..) 迭代屬性-值元組的子列表。內(nèi)側(cè)的迭代函數(shù)是使用 elem 作為第一個參數(shù)對 setElemAttr(..) 進行局部應用,然后對其進行參數(shù)擴散。
到這里,我們有了一個 <span> 元素的列表,每一個都被屬性填充好了,但是還沒有 innerHTML 內(nèi)容。我們使用 stockTickerUI.updateStockElems(..) 將 data 設(shè)置到 <span> 子元素,和證券更新事件一樣。
現(xiàn)在,我們需要將這些 <span> 追加到父節(jié)點 <li> 上去,而且我們使用 reduce(..)(參見第八章)來這樣做:
reduce( appendDOMChild )( stockElem )( infoChildElems );
最后,用一個老式的 DOM 變更副作用將新的證券元組追加到控件的 DOM 上:
tickerElem.appendChild( stockElem );
咻!你都跟上了?在前進之前,我建議你回過頭去將這次討論重讀幾分鐘并實踐一下代碼。
訂閱 Observable
我們最后的主要任務是訂閱定義在 ch11-code/stock-ticker-events.js 中的 observable,并將這些訂閱內(nèi)容連接在恰當?shù)闹?UI 方法(addStock(..) 和 updateStock(..))上。
首先,我們注意到這些方法每個都期待 tickerElem 作為第一個參數(shù)。我們來制造一個列表(stockTickerUIMethodsWithDOMContext),它通過局部應用(也就是閉包;參見第二章)將 ticker 控件的 DOM 元素與這兩個方法封裝起來:
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(..) 它以便提前制定第二個參數(shù) ticker;當每個 UI 方法之后被映射時,它會使用 ticker 對這個函數(shù)進行局部應用?,F(xiàn)在,這兩個在結(jié)果數(shù)組中的雙重局部應用函數(shù)可以用于訂閱 observable 了。
雖然我們使用了閉包來將 ticker 的狀態(tài)保留在這兩個函數(shù)中,但是在第七章中我們看到了可以將這個 ticker 值作為一個對象上的屬性 “保留” 下來,也許是通過用 this 將每個函數(shù)綁定到 stockTickerUI。因為 this 是一種隱含的輸入(參見第二章),而且這通常來說不太好,所以我選擇了閉包而非對象。
為了訂閱 observable,我們來制造一個用于解綁定方法的幫助函數(shù):
var subscribeToObservable =
pipe( uncurry, spreadArgs )( unboundMethod( "subscribe" ) );
unboundMethod("subscribe") 是自動被柯里化的,所以我們 uncurry(..) 它(參見第三章),然后使用 spreadArgs(..) 適配它(同樣參照第三章),這樣它將把一個元組數(shù)組擴散為它的兩個參數(shù)。
現(xiàn)在,我們只需要一個 observable 的列表,這樣我們就可以將它與封裝了上下文環(huán)境的 UI 方法列表 zip(..) 在一起。之后這個元組的列表的每一個都可以使用我們剛剛在前一個代碼段中定義的 subscribeToObservable(..) 進行訂閱:
var stockTickerObservables = [ newStocks, stockUpdates ];
// !!副作用!!
each( subscribeToObservable )
( zip( stockTickerUIMethodsWithDOMContext, stockTickerObservables ) );
因為從技術(shù)上講,為了訂閱這些 observable,我們在改變它們的狀態(tài),而且也因為我們在使用 each(..) —— 幾乎總是與副作用相關(guān)! —— 所以我們在代碼注釋中指出這個事實。
就是這樣!就像早先我們在討論證券報價機事件時做的那樣,花同樣的時間重復閱讀并將這段代碼與它的指令式形式進行比較吧。真的,花些時間來做。我知道這是一本很厚的書,但你讀了這么多說到底是為了能夠消化并理解這樣的代碼。
你對在 JavaScript 中以一種平衡的方式使用 FP 感覺怎么樣?就像我們在這里做的一樣,繼續(xù)練習吧!
總結(jié)
我們在本章中討論的代碼應當作為整體來看待,而不只是像在本章中一樣被打碎成片段。如果還沒這么做過的話,現(xiàn)在就停下去通讀完整的文件吧。確保你能在完整的上下文中理解它們。
這個代碼示例不是要成為你應當如何確切編寫代碼的規(guī)定。它是想要更好地描述如何使用輕量 FP 技術(shù)來思考并著手處理這樣的任務。它想要盡可能多地將本書中的不同概念關(guān)聯(lián)起來。它想要在更 “真實” 的代碼場景下探索 FP,而非我們通常引用的代碼片段。
我十分確信我在自己的旅程中更好地理解了 FP,我將會繼續(xù)改進自己編寫這段實例代碼的方式。你現(xiàn)在看到的只是我的弧線上的一個快照。我希望這對你也一樣。
在我們結(jié)束本書正文之前,我想提醒你的是我在第一章中分享過的這幅可讀性曲線:

在這次學習并在你的 JavaScript 中應用 FP 的旅途中,將這幅圖牢記在心并為你自己設(shè)定現(xiàn)實的目標是十分重要的。你已經(jīng)堅持到了這里,這是十分了不起的成就。
但是當你向著絕望和失落的低谷傾斜的時候不要放棄。在另一邊等著你的是一種對你代碼的思考和交流的方式,它更易于解讀,易于理解,易于驗證,而最終,更加可靠。
對于我們開發(fā)者來說,我想不出值得為之努力的更高尚的目標。感謝你與我分享在 JavaScript 中學習 FP 原理的旅途。希望你的旅途和我的一樣豐富多彩!