繼續探索JS中的Iterator,兼談與Observable的對比
前言
JavaScript 2015中引入了Generator Function(相關內容可以參考前作 ES6 generator函數與co一瞥 與 ES6 generator函數與co再一瞥 ),并且在加入了 Symbol.iterator 之后,使得構造擁有自定義迭代器的集合變得相當容易(可以參考前作 在JavaScript中實現LINQ——一次“失敗”的嘗試 )。
前幾天在群里@徐叔提出了這樣一個問題:
function* listen(element) {
element.addEventListener('click', function(e) {
// 這里怎么把e通過外面的listen給yield出去?
})
}
音錘思婷……
我理解,叔叔寫 listen 的目的是為了把事件源抽象成一個“可以被遍歷的集合”。
JavaScript里的迭代器模式
要理解JS里的迭代器模式,首先必須從 GeneratorFunction 和 Symbol.iterator 說起。
JS的迭代器模式和C#有些許不同(原諒我經常用C#力的接口來做例子,其實只是因為我覺得它這些接口設計得比較工整良好,而且強類型語言也挺適合做例子),C#中使用兩個接口 IEnumerable 和 IEnumerator 來實現迭代器模式,分別定義為
public interface IEnumerable<T> {
IEnumerator<T> GetEnumerator()
}
public interface IEnumerator<T> {
T Current { get; }
bool MoveNext()
// 省略其他無關緊要的
}
實現了 IEnumerable 的類型可以享受到 foreach 語法糖, foreach 展開后就是通過對 IEnumerator 不斷地 MoveNext() 來完成迭代過程,這很好理解。
JS的迭代器模式圍繞 Symbol.iterator ,任何對象只要實現了 Symbol.iterator 就可以享受 for-of 語法糖。
在迭代過程方面,C#只用 IEnumerator 一個接口同時實現了迭代和取值兩個操作,但JS里用了兩個接口,這里舉個例子
var array = [1, 2, 3, 4, 5]
var iter = array[Symbol.iterator]()
for (var it = iter.next(); !it.done; it = iter.next()) {
console.log(it)
}
可以看到調用 Symbol.iterator 所得到的 iter 對象只是負責 next() 工作,而其不斷 next 所得到的 it 對象則負責 value 和 done 工作。
也就是說,在不借助 yield 的情況下,要實現 Symbol.iterator 只需要構造一個滿足上述接口的對象就OK了,舉個例子
var fakeArray = {
_values: [1, 2, 3, 4, 5],
[Symbol.iterator]() {
var _values = this._values
var _index = 0
var iter = {
next() {
var it = {
value: _values[_index],
done: _index >= _values.length
}
if (!it.done) {
_index++
}
return it
}
}
return iter
}
}
for (var n offakeArray) {
console.log(n)
}
然后我們嘗試一下,能不能用 yield * 語法來實現它和 Generator 的無縫銜接:
function* gen() {
yield '1-1'
yield '1-2'
yield* fakeArray
yield '1-3'
}
for (var t ofgen()) {
console.log(t)
}
耶,成功了,解糖后手工遍歷呢?
var iter = gen()
for (var it = iter.next(); !it.done; it = iter.next()) {
console.log(it)
}
用迭代器模式實現事件源是否可行
先說結論,我認為是:僅從上面所討論的范圍來看, 不可行 。
使用迭代器模式,無外乎是為了能工用 for-of 語法(或者解糖以后自己不斷 next() )來遍歷集合。我們知道迭代器模式是一種典型的“Pull”模型,迭代過程是不斷從集合里把東西拉出來,直到什么都拉不出來了(怎么聽起來這么膈應)。
事件源是一個異步的東西,只有當事件發生的時候才會有貨,但我們并不知道事件什么時候發生,因此當被“拉”的時候,不知道該把什么東西交給迭代器。
這時候有同學要問了,之前我們不是用co通過 yield 來處理異步的東西嗎,這不是證明 yield/generator 是可以處理異步問題的嗎?
其實只要看過我之前文章或者對co有了解的同學肯定就會知道,co是對 yield/generator 的“誤用”,我之所以加引號是因為在Unity的C#里甚至官方就直接用 yield 和 IEnumerator 來實現了官方的協程API(我就不吐槽了您趕緊把C#版本升級了用 async/await 吧),據我了解Python也有這么干的。這說明這個“誤用”是一個有據可循的東西。
在co這樣的語境下, yield/generator 已經完全不是為了構造自定義集合以及配合 for-of 語法糖實現迭代器模式而用的,所以我們費了老鼻子勁實現的 Symbol.iterator 到底還有沒有卵用?
我要說,如果跳出上面所討論的范圍來看呢,還是有點兒卵用的。
“黑化”之后的產物
我們先設定一個“目標語法”
function* eventListeningByCoroutine() {
var eventSource = someMagicFunction()
while (true) {
var e = yieldeventSource.take()
document.querySelector('#logger').innerHTML = e.pageX + ', ' + e.pageY
}
}
看到沒,用一個 while (true) ,死命地從 eventSource 里拉東西出來,由于這個拉的過程是不確定(異步)的,我們只好加了 yield 。
所以現在模型建立了,我們剩下兩個問題,一個是 someMagicFunction 如何實現,一個是 startCoroutine 如何實現。
如果看過我之前寫的 ES6 generator函數與co再一瞥 ,嗯,也可以起一個新名字,叫做《手把手教你實現一個山寨的co),那么應該很快就能寫出上面的 startCoroutine 函數。
function startCoroutine(generatorFunction) {
var iter = generatorFunction()
function step(data) {
var it = iter.next(data)
if (it.done) {
return
}
var callback = it.value
callback(function(val) {
step(val)
})
}
step()
}
具體過程就不展開分析了,呃,我的意思是大概這樣↓
然后更關鍵的是 someMagicFunction 怎么實現
function someMagicFunction() {
var taker
var iter = {
take: function() {
return function(callback) {
taker = function(e) {
callback(e)
}
}
}
}
function put(e) {
if (!taker) {
return // dropped
}
var _taker = taker
taker = null // cleaning up
_taker(e)
}
document.querySelector('#main').addEventListener('click', function(e) {
put(e)
})
return iter
}
完整演示在這里 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”的方式實現了呢?因為 startCoroutine 和 someMagicFunction 這兩者之間實現了消息傳遞, 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的時候就開的坑——是啊,在我腦海里開了坑,但沒敢告訴你們,免得你們又吐槽我挖坑不填(逃
來自:http://web.jobbole.com/90938/