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

關於譯者:這是一個流淌着滬江血液的純粹工程:認真,是 HTML 最堅實的樑柱;分享,是 CSS 裏最閃耀的一瞥;總結,是 JavaScript 中最嚴謹的邏輯。通過捶打磨練,成就了本書的中文版。本書包含了函數式編程之精髓,但願能夠幫助你們在學習函數式編程的道路上走的更順暢。比心。前端

譯者團隊(排名不分前後):阿希bluekenbrucechamcfanlifedailkyoko-dfl3velilinsLittlePineappleMatildaJin冬青pobusamaCherry蘿蔔vavd317vivaxy萌萌zhouyaogit

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

響應式函數式編程

爲了理解如何在2個值之間建立和使用惰性的映射,咱們須要去抽象咱們對列表(數組)的想法。github

讓咱們來想象一個智能的數組,不僅是簡單地得到值,仍是一個懶惰地接受和響應(也就是「反應」)值的數組。考慮下:web

var a = new LazyArray();

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

setInterval( function everySecond(){
	a.push( Math.random() );
}, 1000 );
複製代碼

至此,這段代碼的數組和普通的沒有什麼區別。惟一不一樣的是在咱們執行 map(..) 來映射數組 a 生成數組 b 以後,定時器在 a 裏面添加隨機的值。編程

可是這個虛構的 LazyArray 有點不一樣,它假設了值能夠隨時的一個一個添加進去。就像隨時能夠 push(..) 你想要的值同樣。能夠說 b 就是一個惰性映射 a 最終值的數組。小程序

此外,當 a 或者 b 改變時,咱們不須要確切地保存裏面的值,這個特殊的數組將會保存它所需的值。因此這些數組不會隨着時間而佔用更多的內存,這是 惰性數據結構和懶操做的重要特色。事實上,它看起來不像數組,更像是buffer(緩衝區)。微信小程序

普通的數組是積極的,因此它會立馬保存全部它的值。"惰性數組" 的值則會延遲保存。數組

因爲咱們不必定要知道 a 何時添加了新的值,因此另外一個關鍵就是咱們須要有去監聽 b 並在有新值的時候通知它的能力。咱們能夠想象下監聽器是這樣的:promise

b.listen( function onValue(v){
	console.log( v );
} );
複製代碼

b 是反應性的,由於它被設置爲當 a 有值添加時進行反應。函數式編程操做當中的 map(..) 是把數據源 a 裏面的全部值轉移到目標 b 裏。每次映射操做都是咱們使用同步函數式編程進行單值建模的過程,可是接下來咱們將讓這種操做變得能夠響應式執行。緩存

注意: 最經常使用到這些函數式編程的是響應式函數式編程(FRP)。我故意避開這個術語是由於一個有關於 FP + Reactive 是否真的構成 FRP 的辯論。咱們不會全面深刻了解 FRP 的全部含義,因此我會繼續稱之爲響應式函數式編程。或者,若是你不會感受那麼困惑,也能夠稱之爲事件機制函數式編程。

咱們能夠認爲 a 是生成值的而 b 則是去消費這些值的。因此爲了可讀性,咱們得從新整理下這段代碼,讓問題歸結於 生產者消費者

// 生產者:

var a = new LazyArray();

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


// **************************
// 消費者:

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

b.listen( function onValue(v){
	console.log( v );
} );
複製代碼

a 是一個行爲本質上很像數據流的生產者。咱們能夠把每一個值賦給 a 看成一個事件map(..) 操做會觸發 b 上面的 listen(..) 事件來消費新的值。

咱們分離 生產者消費者 的相關代碼,是由於咱們的代碼應該各司其職。這樣的代碼組織能夠很大程度上提升代碼的可讀性和維護性。

聲明式的時間

咱們應該很是謹慎地討論如何介紹時間狀態。具體來講,正如 promise 從單個異步操做中抽離出咱們所擔憂的時間狀態,響應式函數式編程從一系列的值/操做中抽離(分割)了時間狀態。

a (生產者)的角度來講,惟一與時間相關的就是咱們手動調用的 setInterval(..) 循環。但它只是爲了示範。

想象下 a 能夠被綁定上一些其餘的事件源,好比說用戶的鼠標點擊事件和鍵盤按鍵事件,服務端來的 websocket 消息等。在這些狀況下,a 不必關注本身的時間狀態。每當值準備好,它就只是一個與值鏈接的無時態管道。

b (消費者)的角度來講,咱們不用知道或者關注 a 裏面的值在什麼時候何地來的。事實上,全部的值都已經存在。咱們只關注是否不管什麼時候都能取到那些值。或者說,map(..) 的轉換操做是一個無時態(惰性)的建模過程。

時間ab 之間的關係是聲明式的,不是命令式的。

以 operations-over-time 這種方式來組織值可能不是頗有效。讓咱們來對比下相同的功能如何用命令式來表示:

// 生產者:

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

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


// **************************
// 消費者:

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

這彷佛很微妙,但這就是存在於命令式版本的代碼和以前聲明式的版本之間一個很重要的不一樣點,除了 b.onValue(..) 須要本身去調用 this.map(..) 以外。在以前的代碼中, ba 當中去拉取,可是在這個代碼中,a 推送給 b。換句話說,把 b = a.map(..) 替換成 b.onValue(v)

在上面的命令式代碼中,以消費者的角度來講它並不清楚 v 從哪裏來。此外命令式強硬的把代碼 b.onValue(..) 夾雜在生產者 a 的邏輯裏,這有點違反了關注點分離原則。這將會讓分離生產者和消費者變得困難。

相比之下,在以前的代碼中,b = a.map(..) 表示了 b 的值來源於 a ,對於如同抽象事件流的數據源 a,咱們不須要關心。咱們能夠 確信 任何來自於 ab 裏的值都會經過 map(..) 操做。

映射以外的東西

爲了方便,咱們已經說明了經過隨着時間一次一次的用 map(..) 來綁定 ab 的概念。其實咱們許多其餘的函數式編程操做也能夠作到這種效果。

思考下:

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

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

這裏能夠看到 a 的值確定會經過 isOdd(..) 賦值給 b

即便是 reduce(..) 也能夠持續的運行:

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

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

由於咱們調用 reduce(..) 是沒有給具體 initialValue 的值,不管是 sum(..) 或者 runningTotal(..) 都會等到有 2 個來自 a 的參數時纔會被調用。

這段代碼暗示了在 reduction 裏面有一個 內存空間, 每當有新的值進來的時候,sum(..) 纔會帶上第一個參數 total 和第二個參數 v被調用。

其餘的函數式編程操做會在內部做用域請求一個緩存區,好比說 unique(..) 能夠追蹤每個它訪問過的值。

Observables

但願如今你能夠察覺到響應式,事件式,類數組結構的數據的重要性,就像咱們虛構出來的 LazyArray 同樣。值得高興的是,這類的數據結構已經存在的了,它就叫 observable。

注意: 只是作些假設(但願):接下來的討論只是簡要的介紹 observables。這是一個須要咱們花時間去探究的深層次話題。可是若是你理解本文中的輕量級函數式編程,而且知道如何經過函數式編程的原理來構建異步的話,那麼接着學習 observables 將會變得駕輕就熟。

如今已經有各類各樣的 Observables 的庫類, 最出名的是 RxJS 和 Most。在寫這篇文章的時候,正好有一個直接向 JS 裏添加 observables 的建議,就像 promise。爲了演示,咱們將用 RxJS 風格的 Observables 來完成下面的例子。

這是咱們一個較早的響應式的例子,可是用 Observables 來代替 LazyArray

// 生產者:

var a = new Rx.Subject();

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


// **************************
// 消費者:

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

b.subscribe( function onValue(v){
	console.log( v );
} );
複製代碼

在 RxJS 中,一個 Observer 訂閱一個 Observable。若是你把 Observer 和 Observable 的功能結合到一塊兒,那就會獲得一個 Subject。所以,爲了保持代碼的簡潔,咱們把 a 構建成一個 Subject,因此咱們能夠調用它的 next(..) 方法來添加值(事件)到他的數據流裏。

若是咱們要讓 Observer 和 Observable 保持分離:

// 生產者:

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

在這個代碼裏,a 是 Observable,毫無疑問,observer 就是獨立的 observer,它能夠去「觀察」一些事件(好比咱們的setInterval(..)循環),而後咱們使用它的 next(..) 方法來發送一些事件到 observable a 的流裏。

除了 map(..),RxJS 還定義了超過 100 個能夠在有新值添加時才觸發的方法。就像數組同樣。每一個 Observable 的方法都會返回一個新的 Observable,意味着他們是鏈式的。若是一個方法被調用,則它的返回值應該由輸入的 Observable 去返回,而後觸發到輸出的 Observable裏,不然拋棄。

一個鏈式的聲明式 observable 的例子:

var b =
	a
	.filter( v => v % 2 == 1 )      // 過濾掉偶數
	.distinctUntilChanged()         // 過濾連續相同的流
	.throttle( 100 )                // 函數節流(合併100毫秒內的流)
	.map( v = v * 2 );              // 變2倍

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

注意: 這裏的鏈式寫法不是必定要把 observable 賦值給 b 和調用 b.subscribe(..) 分開寫,這樣作只是爲了讓每一個方法都會獲得一個新的返回值。一般,subscribe(..) 方法都會在鏈式寫法的最後被調用。

總結

這本書詳細的介紹了各類各樣的函數式編程操做,例如:把單個值(或者說是一個即時列表的值)轉換到另外一個值裏。

對於那些有時態的操做,全部基礎的函數式編程原理均可以無時態的應用。就像 promise 建立了一個單一的將來值,咱們能夠建立一個積極的列表的值來代替像惰性的observable(事件)流的值。

數組的 map(..) 方法會用當前數組中的每個值運行一次映射函數,而後放到返回的數組裏。而 observable 數組裏則是爲每個值運行一次映射函數,不管這個值什麼時候加入,而後把它返回到 observable 裏。

或者說,若是數組對函數式編程操做是一個積極的數據結構,那麼 observable 至關於持續惰性的。

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

iKcamp原創新書《移動Web前端高效開發實戰》已在亞馬遜、京東、噹噹開售。

iKcamp官網:www.ikcamp.com 訪問官網更快閱讀所有免費分享課程:《iKcamp出品|全網最新|微信小程序|基於最新版1.0開發者工具之初中級培訓教程分享》。 包含:文章、視頻、源代碼


2019年,iKcamp原創新書《Koa與Node.js開發實戰》已在京東、天貓、亞馬遜、噹噹開售啦!

相關文章
相關標籤/搜索
本站公眾號
   歡迎關注本站公眾號,獲取更多信息