继续探索JS中的Iterator,兼谈与Observable的对比
<h2>前言</h2> <p>JavaScript 2015中引入了Generator Function(相关内容可以参考前作 <a href="/misc/goto?guid=4959746577589385039" rel="nofollow,noindex">ES6 generator函数与co一瞥</a> 与 <a href="/misc/goto?guid=4959746577681741684" rel="nofollow,noindex">ES6 generator函数与co再一瞥</a> ),并且在加入了 Symbol.iterator 之后,使得构造拥有自定义迭代器的集合变得相当容易(可以参考前作 <a href="/misc/goto?guid=4959746577773971129" rel="nofollow,noindex">在JavaScript中实现LINQ——一次“失败”的尝试</a> )。</p> <p>前几天在群里@徐叔提出了这样一个问题:</p> <pre> <code class="language-javascript">function* listen(element) { element.addEventListener('click', function(e) { // 这里怎么把e通过外面的listen给yield出去? }) } </code></pre> <p>音锤思婷……</p> <p>我理解,叔叔写 listen 的目的是为了把事件源抽象成一个“可以被遍历的集合”。</p> <h2>JavaScript里的迭代器模式</h2> <p>要理解JS里的迭代器模式,首先必须从 GeneratorFunction 和 Symbol.iterator 说起。</p> <p>JS的迭代器模式和C#有些许不同(原谅我经常用C#力的接口来做例子,其实只是因为我觉得它这些接口设计得比较工整良好,而且强类型语言也挺适合做例子),C#中使用两个接口 IEnumerable 和 IEnumerator 来实现迭代器模式,分别定义为</p> <pre> <code class="language-javascript">public interface IEnumerable<T> { IEnumerator<T> GetEnumerator() } public interface IEnumerator<T> { T Current { get; } bool MoveNext() // 省略其他无关紧要的 } </code></pre> <p>实现了 IEnumerable 的类型可以享受到 foreach 语法糖, foreach 展开后就是通过对 IEnumerator 不断地 MoveNext() 来完成迭代过程,这很好理解。</p> <p>JS的迭代器模式围绕 Symbol.iterator ,任何对象只要实现了 Symbol.iterator 就可以享受 for-of 语法糖。</p> <p>在迭代过程方面,C#只用 IEnumerator 一个接口同时实现了迭代和取值两个操作,但JS里用了两个接口,这里举个例子</p> <pre> <code class="language-javascript">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) } </code></pre> <p>可以看到调用 Symbol.iterator 所得到的 iter 对象只是负责 next() 工作,而其不断 next 所得到的 it 对象则负责 value 和 done 工作。</p> <p>也就是说,在不借助 yield 的情况下,要实现 Symbol.iterator 只需要构造一个满足上述接口的对象就OK了,举个例子</p> <pre> <code class="language-javascript">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) } </code></pre> <p>然后我们尝试一下,能不能用 yield * 语法来实现它和 Generator 的无缝衔接:</p> <pre> <code class="language-javascript">function* gen() { yield '1-1' yield '1-2' yield* fakeArray yield '1-3' } for (var t ofgen()) { console.log(t) } </code></pre> <p>耶,成功了,解糖后手工遍历呢?</p> <pre> <code class="language-javascript">var iter = gen() for (var it = iter.next(); !it.done; it = iter.next()) { console.log(it) } </code></pre> <h2>用迭代器模式实现事件源是否可行</h2> <p>先说结论,我认为是:仅从上面所讨论的范围来看, <strong>不可行</strong> 。</p> <p>使用迭代器模式,无外乎是为了能工用 for-of 语法(或者解糖以后自己不断 next() )来遍历集合。我们知道迭代器模式是一种典型的“Pull”模型,迭代过程是不断从集合里把东西拉出来,直到什么都拉不出来了(怎么听起来这么膈应)。</p> <p>事件源是一个异步的东西,只有当事件发生的时候才会有货,但我们并不知道事件什么时候发生,因此当被“拉”的时候,不知道该把什么东西交给迭代器。</p> <p>这时候有同学要问了,之前我们不是用co通过 yield 来处理异步的东西吗,这不是证明 yield/generator 是可以处理异步问题的吗?</p> <p>其实只要看过我之前文章或者对co有了解的同学肯定就会知道,co是对 yield/generator 的“误用”,我之所以加引号是因为在Unity的C#里甚至官方就直接用 yield 和 IEnumerator 来实现了官方的协程API(我就不吐槽了您赶紧把C#版本升级了用 async/await 吧),据我了解Python也有这么干的。这说明这个“误用”是一个有据可循的东西。</p> <p>在co这样的语境下, yield/generator 已经完全不是为了构造自定义集合以及配合 for-of 语法糖实现迭代器模式而用的,所以我们费了老鼻子劲实现的 Symbol.iterator 到底还有没有卵用?</p> <p>我要说,如果跳出上面所讨论的范围来看呢,还是有点儿卵用的。</p> <h2>“黑化”之后的产物</h2> <p>我们先设定一个“目标语法”</p> <pre> <code class="language-javascript">function* eventListeningByCoroutine() { var eventSource = someMagicFunction() while (true) { var e = yieldeventSource.take() document.querySelector('#logger').innerHTML = e.pageX + ', ' + e.pageY } } </code></pre> <p>看到没,用一个 while (true) ,死命地从 eventSource 里拉东西出来,由于这个拉的过程是不确定(异步)的,我们只好加了 yield 。</p> <p>所以现在模型建立了,我们剩下两个问题,一个是 someMagicFunction 如何实现,一个是 startCoroutine 如何实现。</p> <p>如果看过我之前写的 <a href="/misc/goto?guid=4959746577681741684" rel="nofollow,noindex">ES6 generator函数与co再一瞥</a> ,嗯,也可以起一个新名字,叫做《手把手教你实现一个山寨的co),那么应该很快就能写出上面的 startCoroutine 函数。</p> <pre> <code class="language-javascript">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() } </code></pre> <p>具体过程就不展开分析了,呃,我的意思是大概这样↓</p> <p><img src="https://simg.open-open.com/show/f0fdd611c2bdf32fd63be746969e425f.jpg"></p> <p>然后更关键的是 someMagicFunction 怎么实现</p> <pre> <code class="language-javascript">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 } </code></pre> <p>完整演示在这里 <a href="/misc/goto?guid=4959746577885635155" rel="nofollow,noindex">runjs/yzbro1a1</a> 。</p> <p>嗯,其实我就是劣质地抄了一个 <a href="/misc/goto?guid=4959746577973392524" rel="nofollow,noindex">js-csp</a> ,它是一个 <strong>CSP(Communicating sequential processes)</strong> 的实现,相当于Clojure里的 core.async 和Go里的 chan 。这里的例子也基本就是 <a href="/misc/goto?guid=4959746578070096352" rel="nofollow,noindex">js-csp的其中一个例子</a> 的简化版而已。</p> <p>在CSP中,事件源被抽象为一个 channel (或者像erlang里好像叫mailbox之类的,很形象),发生事件的时候往里面 put ,监听事件这个事情体现为源源不断地(while-true)从里面 take ——注意,这个 take 是一个“阻塞”操作,体现为它必须冠以 yield 。</p> <h2>与 Observable (RxJS)对比</h2> <p>从上面可以看到,只靠迭代器模式是不能用来抽象异步事件源的(至少吧,以我当前的理解能力,是不能的)。</p> <p>本质上是因为迭代器模式使用的是“Pull”模型,什么时候发生迭代完全是由迭代者本身什么时候去“拉”数据决定的;而观察者模式是“Push”模型,什么时候发生迭代是由数据源本身决定的,这也使得它非常适合“事件流”、“消息推送”这类的持续、异步数据的迭代,也就是所谓的“Reactive Programming”。</p> <p>那为什么最后的DEMO就用更类似“Pull”的方式实现了呢?因为 startCoroutine 和 someMagicFunction 这两者之间实现了消息传递, startCoroutine 接管了 yield 和迭代中“什么时候该 next() ”的过程, someMagicFunction 向反过来向它发送“你可以继续拉了”的消息(注意:上面的例子中实现为回调函数),这俩一推一拉,好不默契(???</p> <p>值得注意的一点是不论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。</p> <p>当然最后实现了一个劣质的CSP的DEMO,也算填了一个我两年前学习Go以及第一次看到js-csp的时候就开的坑——是啊,在我脑海里开了坑,但没敢告诉你们,免得你们又吐槽我挖坑不填(逃</p> <p> </p> <p>来自:http://web.jobbole.com/90938/</p> <p> </p>
本文由用户 minihacker 自行上传分享,仅供网友学习交流。所有权归原作者,若您的权利被侵害,请联系管理员。
转载本站原创文章,请注明出处,并保留原始链接、图片水印。
本站是一个以用户分享为主的开源技术平台,欢迎各类分享!