翻譯連載 | 第 10 章:異步的函數(shù)式(下)-《JavaScript輕量級(jí)函數(shù)式編程》 |《你不知道的JS》姊妹篇

關(guān)于譯者:這是一個(gè)流淌著滬江血液的純粹工程:認(rèn)真,是 HTML 最堅(jiān)實(shí)的梁柱藐鹤;分享,是 CSS 里最閃耀的一瞥;總結(jié),是 JavaScript 中最嚴(yán)謹(jǐn)?shù)倪壿嬑Τ=?jīng)過(guò)捶打磨練塘慕,成就了本書的中文版。本書包含了函數(shù)式編程之精髓扑眉,希望可以幫助大家在學(xué)習(xí)函數(shù)式編程的道路上走的更順暢纸泄。比心。

譯者團(tuán)隊(duì)(排名不分先后):阿希腰素、blueken聘裁、brucechamcfanlife弓千、dail衡便、kyoko-dfl3ve洋访、lilins镣陕、LittlePineappleMatildaJin姻政、冬青呆抑、pobusamaCherry汁展、蘿卜鹊碍、vavd317vivaxy食绿、萌萌侈咕、zhouyao

第 10 章:異步的函數(shù)式(下)

響應(yīng)式函數(shù)式編程

為了理解如何在2個(gè)值之間創(chuàng)建和使用惰性的映射,我們需要去抽象我們對(duì)列表(數(shù)組)的想法器紧。

讓我們來(lái)想象一個(gè)智能的數(shù)組耀销,不只是簡(jiǎn)單地獲得值,還是一個(gè)懶惰地接受和響應(yīng)(也就是“反應(yīng)”)值的數(shù)組铲汪⌒芪荆考慮下:

var a = new LazyArray();

var b = a.map( function double(v){
    return v * 2;
} );

setInterval( function everySecond(){
    a.push( Math.random() );
}, 1000 );

至此,這段代碼的數(shù)組和普通的沒(méi)有什么區(qū)別桥状。唯一不同的是在我們執(zhí)行 map(..) 來(lái)映射數(shù)組 a 生成數(shù)組 b 之后帽揪,定時(shí)器在 a 里面添加隨機(jī)的值。

但是這個(gè)虛構(gòu)的 LazyArray 有點(diǎn)不同辅斟,它假設(shè)了值可以隨時(shí)的一個(gè)一個(gè)添加進(jìn)去转晰。就像隨時(shí)可以 push(..) 你想要的值一樣∈快可以說(shuō) b 就是一個(gè)惰性映射 a 最終值的數(shù)組查邢。

此外,當(dāng) a 或者 b 改變時(shí)酵幕,我們不需要確切地保存里面的值扰藕,這個(gè)特殊的數(shù)組將會(huì)保存它所需的值。所以這些數(shù)組不會(huì)隨著時(shí)間而占用更多的內(nèi)存芳撒,這是 惰性數(shù)據(jù)結(jié)構(gòu)和懶操作的重要特點(diǎn)邓深。事實(shí)上未桥,它看起來(lái)不像數(shù)組,更像是buffer(緩沖區(qū))芥备。

普通的數(shù)組是積極的冬耿,所以它會(huì)立馬保存所有它的值。"惰性數(shù)組" 的值則會(huì)延遲保存萌壳。

由于我們不一定要知道 a 什么時(shí)候添加了新的值亦镶,所以另一個(gè)關(guān)鍵就是我們需要有去監(jiān)聽 b 并在有新值的時(shí)候通知它的能力。我們可以想象下監(jiān)聽器是這樣的:

b.listen( function onValue(v){
    console.log( v );
} );

b 是反應(yīng)性的袱瓮,因?yàn)樗辉O(shè)置為當(dāng) a 有值添加時(shí)進(jìn)行反應(yīng)缤骨。函數(shù)式編程操作當(dāng)中的 map(..) 是把數(shù)據(jù)源 a 里面的所有值轉(zhuǎn)移到目標(biāo) b 里。每次映射操作都是我們使用同步函數(shù)式編程進(jìn)行單值建模的過(guò)程尺借,但是接下來(lái)我們將讓這種操作變得可以響應(yīng)式執(zhí)行绊起。

注意: 最常用到這些函數(shù)式編程的是響應(yīng)式函數(shù)式編程(FRP)。我故意避開這個(gè)術(shù)語(yǔ)是因?yàn)橐粋€(gè)有關(guān)于 FP + Reactive 是否真的構(gòu)成 FRP 的辯論褐望。我們不會(huì)全面深入了解 FRP 的所有含義勒庄,所以我會(huì)繼續(xù)稱之為響應(yīng)式函數(shù)式編程串前√崩铮或者,如果你不會(huì)感覺(jué)那么困惑荡碾,也可以稱之為事件機(jī)制函數(shù)式編程谨读。

我們可以認(rèn)為 a 是生成值的而 b 則是去消費(fèi)這些值的。所以為了可讀性坛吁,我們得重新整理下這段代碼劳殖,讓問(wèn)題歸結(jié)于 生產(chǎn)者消費(fèi)者

// 生產(chǎn)者:

var a = new LazyArray();

setInterval( function everySecond(){
    a.push( Math.random() );
}, 1000 );


// **************************
// 消費(fèi)者:

var b = a.map( function double(v){
    return v * 2;
} );

b.listen( function onValue(v){
    console.log( v );
} );

a 是一個(gè)行為本質(zhì)上很像數(shù)據(jù)流的生產(chǎn)者拨脉。我們可以把每個(gè)值賦給 a 當(dāng)作一個(gè)事件哆姻。map(..) 操作會(huì)觸發(fā) b 上面的 listen(..) 事件來(lái)消費(fèi)新的值。

我們分離 生產(chǎn)者消費(fèi)者 的相關(guān)代碼玫膀,是因?yàn)槲覀兊拇a應(yīng)該各司其職矛缨。這樣的代碼組織可以很大程度上提高代碼的可讀性和維護(hù)性。

聲明式的時(shí)間

我們應(yīng)該非常謹(jǐn)慎地討論如何介紹時(shí)間狀態(tài)帖旨。具體來(lái)說(shuō)箕昭,正如 promise 從單個(gè)異步操作中抽離出我們所擔(dān)心的時(shí)間狀態(tài),響應(yīng)式函數(shù)式編程從一系列的值/操作中抽離(分割)了時(shí)間狀態(tài)解阅。

a (生產(chǎn)者)的角度來(lái)說(shuō)落竹,唯一與時(shí)間相關(guān)的就是我們手動(dòng)調(diào)用的 setInterval(..) 循環(huán)。但它只是為了示范货抄。

想象下 a 可以被綁定上一些其他的事件源述召,比如說(shuō)用戶的鼠標(biāo)點(diǎn)擊事件和鍵盤按鍵事件朱转,服務(wù)端來(lái)的 websocket 消息等。在這些情況下积暖,a 沒(méi)必要關(guān)注自己的時(shí)間狀態(tài)肋拔。每當(dāng)值準(zhǔn)備好,它就只是一個(gè)與值連接的無(wú)時(shí)態(tài)管道呀酸。

b (消費(fèi)者)的角度來(lái)說(shuō)凉蜂,我們不用知道或者關(guān)注 a 里面的值在何時(shí)何地來(lái)的。事實(shí)上性誉,所有的值都已經(jīng)存在窿吩。我們只關(guān)注是否無(wú)論何時(shí)都能取到那些值〈砝溃或者說(shuō)纫雁,map(..) 的轉(zhuǎn)換操作是一個(gè)無(wú)時(shí)態(tài)(惰性)的建模過(guò)程。

時(shí)間ab 之間的關(guān)系是聲明式的倾哺,不是命令式的轧邪。

以 operations-over-time 這種方式來(lái)組織值可能不是很有效。讓我們來(lái)對(duì)比下相同的功能如何用命令式來(lái)表示:

// 生產(chǎn)者:

var a = {
    onValue(v){
        b.onValue( v );
    }
};

setInterval( function everySecond(){
    a.onValue( Math.random() );
}, 1000 );


// **************************
// 消費(fèi)者:

var b = {
    map(v){
        return v * 2;
    },
    onValue(v){
        v = this.map( v );
        console.log( v );
    }
};

這似乎很微妙羞海,但這就是存在于命令式版本的代碼和之前聲明式的版本之間一個(gè)很重要的不同點(diǎn)忌愚,除了 b.onValue(..) 需要自己去調(diào)用 this.map(..) 之外。在之前的代碼中却邓, ba 當(dāng)中去拉取硕糊,但是在這個(gè)代碼中,a 推送給 b腊徙。換句話說(shuō)简十,把 b = a.map(..) 替換成 b.onValue(v)

在上面的命令式代碼中撬腾,以消費(fèi)者的角度來(lái)說(shuō)它并不清楚 v 從哪里來(lái)螟蝙。此外命令式強(qiáng)硬的把代碼 b.onValue(..) 夾雜在生產(chǎn)者 a 的邏輯里,這有點(diǎn)違反了關(guān)注點(diǎn)分離原則民傻。這將會(huì)讓分離生產(chǎn)者和消費(fèi)者變得困難胰默。

相比之下,在之前的代碼中饰潜,b = a.map(..) 表示了 b 的值來(lái)源于 a 初坠,對(duì)于如同抽象事件流的數(shù)據(jù)源 a,我們不需要關(guān)心彭雾。我們可以 確信 任何來(lái)自于 ab 里的值都會(huì)通過(guò) map(..) 操作碟刺。

映射之外的東西

為了方便,我們已經(jīng)說(shuō)明了通過(guò)隨著時(shí)間一次一次的用 map(..) 來(lái)綁定 ab 的概念薯酝。其實(shí)我們?cè)S多其他的函數(shù)式編程操作也可以做到這種效果半沽。

思考下:

var b = a.filter( function isOdd(v) {
    return v % 2 == 1;
} );

b.listen( function onlyOdds(v){
    console.log( "Odd:", v );
} );

這里可以看到 a 的值肯定會(huì)通過(guò) isOdd(..) 賦值給 b爽柒。

即使是 reduce(..) 也可以持續(xù)的運(yùn)行:

var b = a.reduce( function sum(total,v){
    return total + v;
} );

b.listen( function runningTotal(v){
    console.log( "New current total:", v );
} );

因?yàn)槲覀冋{(diào)用 reduce(..) 是沒(méi)有給具體 initialValue 的值,無(wú)論是 sum(..) 或者 runningTotal(..) 都會(huì)等到有 2 個(gè)來(lái)自 a 的參數(shù)時(shí)才會(huì)被調(diào)用者填。

這段代碼暗示了在 reduction 里面有一個(gè) 內(nèi)存空間浩村, 每當(dāng)有新的值進(jìn)來(lái)的時(shí)候,sum(..) 才會(huì)帶上第一個(gè)參數(shù) total 和第二個(gè)參數(shù) v被調(diào)用占哟。

其他的函數(shù)式編程操作會(huì)在內(nèi)部作用域請(qǐng)求一個(gè)緩存區(qū)心墅,比如說(shuō) unique(..) 可以追蹤每一個(gè)它訪問(wèn)過(guò)的值。

Observables

希望現(xiàn)在你可以察覺(jué)到響應(yīng)式榨乎,事件式怎燥,類數(shù)組結(jié)構(gòu)的數(shù)據(jù)的重要性,就像我們虛構(gòu)出來(lái)的 LazyArray 一樣蜜暑。值得高興的是铐姚,這類的數(shù)據(jù)結(jié)構(gòu)已經(jīng)存在的了,它就叫 observable肛捍。

注意: 只是做些假設(shè)(希望):接下來(lái)的討論只是簡(jiǎn)要的介紹 observables隐绵。這是一個(gè)需要我們花時(shí)間去探究的深層次話題。但是如果你理解本文中的輕量級(jí)函數(shù)式編程拙毫,并且知道如何通過(guò)函數(shù)式編程的原理來(lái)構(gòu)建異步的話依许,那么接著學(xué)習(xí) observables 將會(huì)變得得心應(yīng)手。

現(xiàn)在已經(jīng)有各種各樣的 Observables 的庫(kù)類恬偷, 最出名的是 RxJS 和 Most悍手。在寫這篇文章的時(shí)候,正好有一個(gè)直接向 JS 里添加 observables 的建議袍患,就像 promise。為了演示竣付,我們將用 RxJS 風(fēng)格的 Observables 來(lái)完成下面的例子诡延。

這是我們一個(gè)較早的響應(yīng)式的例子,但是用 Observables 來(lái)代替 LazyArray

// 生產(chǎn)者:

var a = new Rx.Subject();

setInterval( function everySecond(){
    a.next( Math.random() );
}, 1000 );


// **************************
// 消費(fèi)者:

var b = a.map( function double(v){
    return v * 2;
} );

b.subscribe( function onValue(v){
    console.log( v );
} );

在 RxJS 中古胆,一個(gè) Observer 訂閱一個(gè) Observable肆良。如果你把 Observer 和 Observable 的功能結(jié)合到一起,那就會(huì)得到一個(gè) Subject逸绎。因此惹恃,為了保持代碼的簡(jiǎn)潔,我們把 a 構(gòu)建成一個(gè) Subject棺牧,所以我們可以調(diào)用它的 next(..) 方法來(lái)添加值(事件)到他的數(shù)據(jù)流里巫糙。

如果我們要讓 Observer 和 Observable 保持分離:

// 生產(chǎn)者:

var a = Rx.Observable.create( function onObserve(observer){
    setInterval( function everySecond(){
        observer.next( Math.random() );
    }, 1000 );
} );

在這個(gè)代碼里,a 是 Observable颊乘,毫無(wú)疑問(wèn)参淹,observer 就是獨(dú)立的 observer醉锄,它可以去“觀察”一些事件(比如我們的setInterval(..)循環(huán)),然后我們使用它的 next(..) 方法來(lái)發(fā)送一些事件到 observable a 的流里浙值。

除了 map(..)恳不,RxJS 還定義了超過(guò) 100 個(gè)可以在有新值添加時(shí)才觸發(fā)的方法。就像數(shù)組一樣开呐。每個(gè) Observable 的方法都會(huì)返回一個(gè)新的 Observable烟勋,意味著他們是鏈?zhǔn)降摹H绻粋€(gè)方法被調(diào)用筐付,則它的返回值應(yīng)該由輸入的 Observable 去返回神妹,然后觸發(fā)到輸出的 Observable里,否則拋棄家妆。

一個(gè)鏈?zhǔn)降穆暶魇?observable 的例子:

var b =
    a
    .filter( v => v % 2 == 1 )      // 過(guò)濾掉偶數(shù)
    .distinctUntilChanged()         // 過(guò)濾連續(xù)相同的流
    .throttle( 100 )                // 函數(shù)節(jié)流(合并100毫秒內(nèi)的流)
    .map( v = v * 2 );              // 變2倍

b.subscribe( function onValue(v){
    console.log( "Next:", v );
} );

注意: 這里的鏈?zhǔn)綄懛ú皇且欢ㄒ?observable 賦值給 b 和調(diào)用 b.subscribe(..) 分開寫鸵荠,這樣做只是為了讓每個(gè)方法都會(huì)得到一個(gè)新的返回值。通常伤极,subscribe(..) 方法都會(huì)在鏈?zhǔn)綄懛ǖ淖詈蟊徽{(diào)用蛹找。

總結(jié)

這本書詳細(xì)的介紹了各種各樣的函數(shù)式編程操作,例如:把單個(gè)值(或者說(shuō)是一個(gè)即時(shí)列表的值)轉(zhuǎn)換到另一個(gè)值里哨坪。

對(duì)于那些有時(shí)態(tài)的操作庸疾,所有基礎(chǔ)的函數(shù)式編程原理都可以無(wú)時(shí)態(tài)的應(yīng)用。就像 promise 創(chuàng)建了一個(gè)單一的未來(lái)值当编,我們可以創(chuàng)建一個(gè)積極的列表的值來(lái)代替像惰性的observable(事件)流的值届慈。

數(shù)組的 map(..) 方法會(huì)用當(dāng)前數(shù)組中的每一個(gè)值運(yùn)行一次映射函數(shù),然后放到返回的數(shù)組里忿偷。而 observable 數(shù)組里則是為每一個(gè)值運(yùn)行一次映射函數(shù)金顿,無(wú)論這個(gè)值何時(shí)加入,然后把它返回到 observable 里鲤桥。

或者說(shuō)揍拆,如果數(shù)組對(duì)函數(shù)式編程操作是一個(gè)積極的數(shù)據(jù)結(jié)構(gòu),那么 observable 相當(dāng)于持續(xù)惰性的茶凳。

** 【上一章】翻譯連載 | 第 10 章:異步的函數(shù)式(上)-《JavaScript輕量級(jí)函數(shù)式編程》 |《你不知道的JS》姊妹篇 **

iKcamp原創(chuàng)新書《移動(dòng)Web前端高效開發(fā)實(shí)戰(zhàn)》已在亞馬遜嫂拴、京東、當(dāng)當(dāng)開售贮喧。

iKcamp官網(wǎng):http://www.ikcamp.com
訪問(wèn)官網(wǎng)更快閱讀全部免費(fèi)分享課程:《iKcamp出品|全網(wǎng)最新|微信小程序|基于最新版1.0開發(fā)者工具之初中級(jí)培訓(xùn)教程分享》筒狠。
包含:文章、視頻箱沦、源代碼

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末辩恼,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌运挫,老刑警劉巖状共,帶你破解...
    沈念sama閱讀 221,635評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異谁帕,居然都是意外死亡峡继,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,543評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門匈挖,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)碾牌,“玉大人,你說(shuō)我怎么就攤上這事儡循〔奥穑” “怎么了?”我有些...
    開封第一講書人閱讀 168,083評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵择膝,是天一觀的道長(zhǎng)誓琼。 經(jīng)常有香客問(wèn)我,道長(zhǎng)肴捉,這世上最難降的妖魔是什么腹侣? 我笑而不...
    開封第一講書人閱讀 59,640評(píng)論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮齿穗,結(jié)果婚禮上傲隶,老公的妹妹穿的比我還像新娘。我一直安慰自己窃页,他們只是感情好跺株,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,640評(píng)論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著脖卖,像睡著了一般乒省。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上胚嘲,一...
    開封第一講書人閱讀 52,262評(píng)論 1 308
  • 那天作儿,我揣著相機(jī)與錄音,去河邊找鬼馋劈。 笑死,一個(gè)胖子當(dāng)著我的面吹牛晾嘶,可吹牛的內(nèi)容都是我干的妓雾。 我是一名探鬼主播,決...
    沈念sama閱讀 40,833評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼垒迂,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼械姻!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起机断,我...
    開封第一講書人閱讀 39,736評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤楷拳,失蹤者是張志新(化名)和其女友劉穎绣夺,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體欢揖,經(jīng)...
    沈念sama閱讀 46,280評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡陶耍,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,369評(píng)論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了她混。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片烈钞。...
    茶點(diǎn)故事閱讀 40,503評(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,870評(píng)論 3 333
  • 文/蒙蒙 一佃扼、第九天 我趴在偏房一處隱蔽的房頂上張望偎巢。 院中可真熱鬧,春花似錦兼耀、人聲如沸压昼。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,340評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)窍霞。三九已至,卻和暖如春拯坟,著一層夾襖步出監(jiān)牢的瞬間但金,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,460評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工郁季, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留冷溃,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,909評(píng)論 3 376
  • 正文 我出身青樓梦裂,卻偏偏與公主長(zhǎng)得像似枕,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子年柠,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,512評(píng)論 2 359

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