适合JavaScript 1.7中迭代生成器的异步编程机制
2010-12-03 19:09 by 老赵, 3470 visits上篇文章我提出了一种基于JavaScript 1.7中迭代生成器(yield)的异步编程方式,它可以让混乱的异步代码逻辑变得清晰一些。不过之前的AsyncIterator其实是对基于C# 2.0的AsyncEnumerator的仿制品,在公司的分享会上进行交流以后,同事hax提出其实可以实现地更漂亮一些。在他的提示下,我了解到JavaScript 1.7中不同于C# 2.0里的特性,因而对这种异步编程机制提出了改进。只可惜yield特性被ECMAScript 5排除了,这实在可以说是委员会设计模式的又一次伟大胜利。
JavaScript 1.7的yield
与C#中yield功能不同的是,JavaScript 1.7的yield语句可以让生成器内部获得一个值。例如这样的代码:
var numbers = function (min, max) { for (var i = min; i <= max; i++) { var sum = yield i; document.write(sum + "<br />"); } } var iterator = numbers(1, 10); var sum = iterator.next(); try { while (true) { var i = iterator.send(sum); document.write(i + " - "); sum += i; } } catch (err if err instanceof StopIteration) { }
执行后的结果是:
1 2 - 3 3 - 6 4 - 10 5 - 15 6 - 21 7 - 28 8 - 36 9 - 45 10 - 55
在JavaScript 1.7中,yield除了停止流程,将控制权交给外部之外,还能够返回一个值。这个值由外部控制的代码send至迭代器内部——不过,“启动”一个迭代器则不能使用带参数的send。如上面的代码,我们在while循环外使用next启动一个迭代器。
异步编程模型
在上一篇文章中,我们把异步编程模型统一为:
var beginXxx = function (arg0, arg1, ..., callback) { ... }
调用这个异步方法时,我们需要显示地提供一个callback回调函数。而如今我们已经有了进一步的打算,则需要对其进行修改,如下所示:
var xxxAsync = function (arg1, arg2, ...) { return { start: function (callback) { ... } } }
简单地说,以前的beginXxx方法会直接发起一个异步请求,而如今的xxxAsync方法,则是返回一个表示“异步任务”的对象,这个对象上有一个start方法,接受一个callback函数作为参数,并启动这个异步任务。待异步任务完成时,使用callback函数发起通知并回传结果。基于这个异步模型,我们可以封装一些常用的“异步任务”,例如:
var sleepAsync = function (ms) { return { start : function (callback) { window.setTimeout(callback, ms); } }; }
sleepAsync获得一个“休眠”的异步任务,执行时便会使用setTimeout计时回调。另一个常见的异步任务则是AJAX请求:
XMLHttpRequest.prototype.receiveAsync = function() { var _this = this; return { start : function(callback) { _this.onreadystatechange = function() { if (this.readyState == 4) { callback(_this.responseText); } } _this.send(); } }; }
我为XMLHttpRequest做了扩展,它的receiveAsync方法将获得一个异步任务,这个异步任务的执行结果便是这个请求的responseText值。
辅助类:AsyncTask
之前的辅助对象是AsyncIterator,而现在我们只需根据一个迭代器新建一个异步任务即可:
var AsyncTask = function(iterator) { this._iterator = iterator; this._callback = null; } AsyncTask.prototype._loop = function(result) { var _this = this; try { var task = this._iterator.send(result); task.start(function(r) { _this._loop(r); }); } catch (err if err instanceof StopIteration) { if (this._callback) this._callback(); } } AsyncTask.prototype.start = function(callback) { this._callback = callback; this._loop(); }
作为符合我们异步编程模型的对象,AsyncTask也包含一个start方法,它会调用_loop,在_loop中则从迭代器里获得下一个异步任务,启动,并在它执行结束时继续调用_loop自身。在新的异步模型中,构建一个辅助类库也变得非常容易。
示例1:移动HTML元素
还是使用上次的例子。实现一个移动HTML元素的逻辑其实很简单,只要根据一个时间间隔不断地改变其top和left即可。例如这样:
// Pseudocode, cannot work var move = function (e, startPos, endPos, duration) { for (var t = 0; t < duration; t += 50) { e.style.left = startPos.x + (endPos.x - startPos.x) * t / duration; e.style.top = startPos.y + (endPos.y - startPos.y) * t / duration; sleep(50); // cannot sleep } e.style.left = endPos.x; e.style.top = endPos.y; }
只可惜上面这段代码是无法运行的,因为在浏览器里我们没有任何手段让当前的工作线程暂停,我们没有一个阻塞的同步的sleep方法。不过我们现在有了sleepAsync方法可以提供一个异步任务。因此,moveAsync方法只能这样编写:
var moveAsync = function(e, startPos, endPos, duration) { return { start: function(callback) { var t = 0; var loop = function() { if (t < duration) { t += 50; e.style.left = startPos.x + (endPos.x - startPos.x) * t / duration; e.style.top = startPos.y + (endPos.y - startPos.y) * t / duration; sleepAsync(50).start(loop); } else { if (callback) callback(); } } loop(); } }; }
我们无法使用for循环,只能把循环拆成loop回调。这就是异步代码破坏了代码局部性的例证。可能有些朋友会觉得这样的代码写起来没什么困难的,那么如果再加上if…else或是try…catch呢?不管怎么样,这段代码破坏了程序员编程思路,我觉得实在太丑了。幸好,使用AsyncTask和迭代生成器之后代码完全不需要这么写:
// beginMove with AsyncTask var moveAsync2 = function(e, startPos, endPos, duration) { var generator = function() { for (var t = 0; t < duration; t += 50) { e.style.left = startPos.x + (endPos.x - startPos.x) * t / duration; e.style.top = startPos.y + (endPos.y - startPos.y) * t / duration; yield sleepAsync(50); } e.style.left = endPos.x; e.style.top = endPos.y; }; return new AsyncTask(generator()); }
调用beginSleep之后代码使用yield将控制权交还给了AsyncIterator,而beginSleep完成之后也会通知AsyncIterator并继续这段逻辑。有了yield,我们的代码编写起来便顺畅多了。与之前yield只是用作“交出控制权”相比,如今的yield直接返回一个异步任务,可谓干净清爽,语义优雅。
示例二:获取多个URL的内容
如果有一个URL数组,要编写一个异步方法,返回这个URL数组对应的请求内容。如今我们可以这么写:
var receiveAllAsync = function(urls) { var result = []; var generator = function() { for (var i = 0; i < urls.length; i++) { var req = new XMLHttpRequest(); req.open("GET", urls[i]); var content = yield req.receiveAsync(); result.push(content); } }; return { start: function(callback) { (new AsyncTask()).start(function() { callback(result); }); } }; }
与上例有所不同的是,我们通过yield语句直接得到了receiveAsync异步任务的结果。比较可惜的一点是,无论是之前的AsyncIterator还是现在的AsyncTask都对需要返回值的异步方法不是十分友好,写法有些绕。这也是yield受到功能本身的限制,可见语言特性的确对编程模型的塑造有十分重要的影响:C# 2.0的异步编程模型不如JavaScript 1.7,JavaScript 1.7的异步编程模型却不如Jscex。
总结
总体而言,有了JavaScript 1.7中的迭代生成器,在许多时候已经可以极大地简化异步操作的编程体验了。只可惜JavaScript 1.7只是Mozilla一家的语言,虽然在ECMAScript 4中也有类似功能,但在ECMAScript 5却已经被废弃了。在可预见的将来,这个语言特性不会被各浏览器或是JavaScript引擎所接受。实在可以说是委员会设计模式的又一次伟大胜利——他们就是不希望开发人员的生活能够好过一些。
所以我才做了 lofn 的 yield 功能啊…… 是编译时膨胀展开代码。