繼續探索JS中的Iterator,兼談與Observable的對比

發表於2017-04-01

前言

JavaScript 2015中引入了Generator Function(相關內容可以參考前作ES6 generator函式與co一瞥ES6 generator函式與co再一瞥),並且在加入了Symbol.iterator之後,使得構造擁有自定義迭代器的集合變得相當容易(可以參考前作在JavaScript中實現LINQ——一次“失敗”的嘗試)。

前幾天在群裡@徐叔提出了這樣一個問題:

音錘思婷……

我理解,叔叔寫listen的目的是為了把事件源抽象成一個“可以被遍歷的集合”。

JavaScript裡的迭代器模式

要理解JS裡的迭代器模式,首先必須從GeneratorFunctionSymbol.iterator說起。

JS的迭代器模式和C#有些許不同(原諒我經常用C#力的介面來做例子,其實只是因為我覺得它這些介面設計得比較工整良好,而且強型別語言也挺適合做例子),C#中使用兩個介面IEnumerableIEnumerator來實現迭代器模式,分別定義為

實現了IEnumerable的型別可以享受到foreach語法糖,foreach展開後就是通過對IEnumerator不斷地MoveNext()來完成迭代過程,這很好理解。

JS的迭代器模式圍繞Symbol.iterator,任何物件只要實現了Symbol.iterator就可以享受for-of語法糖。

在迭代過程方面,C#只用IEnumerator一個介面同時實現了迭代和取值兩個操作,但JS裡用了兩個介面,這裡舉個例子

可以看到呼叫Symbol.iterator所得到的iter物件只是負責next()工作,而其不斷next所得到的it物件則負責valuedone工作。

也就是說,在不借助yield的情況下,要實現Symbol.iterator只需要構造一個滿足上述介面的物件就OK了,舉個例子

然後我們嘗試一下,能不能用yield *語法來實現它和Generator的無縫銜接:

耶,成功了,解糖後手工遍歷呢?

用迭代器模式實現事件源是否可行

先說結論,我認為是:僅從上面所討論的範圍來看,不可行

使用迭代器模式,無外乎是為了能工用for-of語法(或者解糖以後自己不斷next())來遍歷集合。我們知道迭代器模式是一種典型的“Pull”模型,迭代過程是不斷從集合裡把東西拉出來,直到什麼都拉不出來了(怎麼聽起來這麼膈應)。

事件源是一個非同步的東西,只有當事件發生的時候才會有貨,但我們並不知道事件什麼時候發生,因此當被“拉”的時候,不知道該把什麼東西交給迭代器。

這時候有同學要問了,之前我們不是用co通過yield來處理非同步的東西嗎,這不是證明yield/generator是可以處理非同步問題的嗎?

其實只要看過我之前文章或者對co有了解的同學肯定就會知道,co是對yield/generator的“誤用”,我之所以加引號是因為在Unity的C#裡甚至官方就直接用yieldIEnumerator來實現了官方的協程API(我就不吐槽了您趕緊把C#版本升級了用async/await吧),據我瞭解Python也有這麼幹的。這說明這個“誤用”是一個有據可循的東西。

在co這樣的語境下,yield/generator已經完全不是為了構造自定義集合以及配合for-of語法糖實現迭代器模式而用的,所以我們費了老鼻子勁實現的Symbol.iterator到底還有沒有卵用?

我要說,如果跳出上面所討論的範圍來看呢,還是有點兒卵用的。

“黑化”之後的產物

我們先設定一個“目標語法”

看到沒,用一個while (true),死命地從eventSource里拉東西出來,由於這個拉的過程是不確定(非同步)的,我們只好加了yield

所以現在模型建立了,我們剩下兩個問題,一個是someMagicFunction如何實現,一個是startCoroutine如何實現。

如果看過我之前寫的ES6 generator函式與co再一瞥,嗯,也可以起一個新名字,叫做《手把手教你實現一個山寨的co),那麼應該很快就能寫出上面的startCoroutine函式。

具體過程就不展開分析了,呃,我的意思是大概這樣↓

怎樣畫馬

然後更關鍵的是someMagicFunction怎麼實現

完整演示在這裡runjs/yzbro1a1

嗯,其實我就是劣質地抄了一個js-csp,它是一個CSP(Communicating sequential processes)的實現,相當於Clojure裡的core.async和Go裡的chan。這裡的例子也基本就是js-csp的其中一個例子的簡化版而已。

在CSP中,事件源被抽象為一個channel(或者像erlang裡好像叫mailbox之類的,很形象),發生事件的時候往裡面put,監聽事件這個事情體現為源源不斷地(while-true)從裡面take——注意,這個take是一個“阻塞”操作,體現為它必須冠以yield

Observable(RxJS)對比

從上面可以看到,只靠迭代器模式是不能用來抽象非同步事件源的(至少吧,以我當前的理解能力,是不能的)。

本質上是因為迭代器模式使用的是“Pull”模型,什麼時候發生迭代完全是由迭代者本身什麼時候去“拉”資料決定的;而觀察者模式是“Push”模型,什麼時候發生迭代是由資料來源本身決定的,這也使得它非常適合“事件流”、“訊息推送”這類的持續、非同步資料的迭代,也就是所謂的“Reactive Programming”。

那為什麼最後的DEMO就用更類似“Pull”的方式實現了呢?因為startCoroutinesomeMagicFunction這兩者之間實現了訊息傳遞,startCoroutine接管了yield和迭代中“什麼時候該next()”的過程,someMagicFunction向反過來向它傳送“你可以繼續拉了”的訊息(注意:上面的例子中實現為回撥函式),這倆一推一拉,好不默契(???

值得注意的一點是不論CSP還是Observable都會存在一個“什麼時候push”的問題,在RxJS和js-csp中,體現為它們有一個Scheduler的存在,在RxJS中它決定subscribe什麼時候被髮射,在js-csp中它決定taker什麼時候被滿足。RxJS內建的Scheduler就有諸如Rx.Scheduler.immediate, Rx.Scheduler.currentThread, Rx.Scheduler.default等好幾種,並且對於不同的Observable它根據策略會預設選擇不同的Scheduler。

當然最後實現了一個劣質的CSP的DEMO,也算填了一個我兩年前學習Go以及第一次看到js-csp的時候就開的坑——是啊,在我腦海裡開了坑,但沒敢告訴你們,免得你們又吐槽我挖坑不填(逃

相關文章