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

minihacker 7年前發布 | 15K 次閱讀 JavaScript開發 JavaScript

前言

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/

 

 本文由用戶 minihacker 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!