JavaScript版本的AsyncEnumerator
地球人都知道,在C# 2.0里提供了yield关键字,可以方便好用地生成一个迭代器,更可以简化异步操作――这是因为有了Jeffrey Richter开发的AsyncEnumerator。在接下来的某些演讲中我准备的主题是“异步编程模型”的演变,自然少不了这非常重要的一环。为了便于广大人民群众更好地接受,我决定使用JavaScript来进行说明。为此,我用JavaScript实现了一个AsyncEnumerator。
JavaScript 1.7里的Iterator生成器
AsyncEnumerator的关键在于yield,yield可以让开发者轻易实现出一个迭代器。只可惜在ECMA-262里并没有定义这个功能,还好在FireFox 2.0之后里实现了JavaScript 1.7,其中便提供了Iterator的生成器。这里我先做一个简单的介绍。
例如,基于JavaScript 1.7实现无限长的菲波纳妾数列,只要:
function fibonacci() {
var fn1 = 1;
var fn2 = 1;
while (true) {
var current = fn2;
fn2 = fn1;
fn1 = fn1 + current;
yield current;
}
}
在使用的时候,JavaScript 1.7里也提供了比较方便的语法,例如:
for (var i in fibonacci()) {
print(i);
}
就像C#中的foreach是使用了MoveNext方法和Current属性一样,其实JavaScript 1.7中针对迭代器的for…in语法也是使用了迭代器对象上的next方法。例如上面的代码便可以修改为:
var iterator = fibonacci();
while (true) {
print(iterator.next());
}
您可能会想,这为什么是一个死循环?这是因为在JavaScript 1.7中,而每次调用next方法都会得到yield返回的某一项,同时通过抛出“StopIteration”来表示“迭代终止”。一般来说不需要手动维护异常,因为这些都交给for…in处理了。
异步模型
无论是哪种“异步编程模型”,首先都需要总结出一种通用的“异步任务模型”,例如.NET中的Begin/End或是基于事件的异步模型。为了简化问题,我在这里提出一种最为简单的异步编程:每个异步操作都是以下形式的:
beginXxx(arg0, arg1, ..., callback);
例如我们来扩展一下XMLHttpRequest类型,提供一个beginReceive方法,最终返回responseText:
XMLHttpRequest.prototype.beginReceive = function (callback) {
this.onreadystatechange = function () {
if (this.readyState == 4) {
callback(this.responseText);
}
}
this.send();
}
同样,我们提供一个beginSleep方法,它仅仅是一个setTimeout函数的简单封装:
var beginSleep = function (ms, callback) {
window.setTimeout(callback, ms);
}
调用beginXxx方法则表示发起一个异步的Xxx操作,其结果会通过callback回调函数传递过来。“回调”是“异步”的精髓,尽管它经常让我们痛苦万分。一般来说,无论是何种异步模型(甚至不关.NET还是JavaScript),最终都是基于回调函数的,最终其实也可以归纳成这里的异步调用形式。
AsyncIterator
下面我们便来仿造AsyncEnumerator编写一个AsyncIterator。AsyncEnumerator的原理很简单,我们在这里继续将其简化,由于是JavaScript,我们还不需要处理多线程之间的竞争,可谓再简单不过了:
var AsyncIterator = function () {
this._result = null;
this._callback = null;
this._iterator = null;
}
AsyncIterator.prototype.callback = function () {
var _this = this;
return function (result) {
_this._result = result;
try {
_this._iterator.next();
} catch (e) {
_this._callback();
}
};
}
AsyncIterator.prototype.result = function () {
return this._result;
}
AsyncIterator.prototype.beginInvoke = function (iterator, callback) {
this._iterator = iterator;
this._callback = callback;
iterator.next();
}
AsyncIterator的原理和使用方式可以用以下几句话描述清楚:
为了便于使用,我为AsyncIterator提供一个“静态方法”:
AsyncIterator.beginInvoke = function (generator, callback) {
var ai = new AsyncIterator();
var iterator = generator(ai);
ai.beginInvoke(iterator, callback);
}
接下来,我们便来看两个实例。
示例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方法,只有一个异步的beginSleep方法(如上文)。因此,其实beginMove方法只能这样编写:
var beginMove = function (e, startPos, endPos, duration, 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;
beginSleep(50, loop);
} else {
callback();
}
}
loop();
}
我们无法使用for循环,只能把循环拆成loop回调。这就是异步代码破坏了代码局部性的例证。可能有些朋友会觉得这样的代码写起来没什么困难的,那么如果再加上if…else或是try…catch呢?不管怎么样,这段代码破坏了程序员编程思路,我觉得实在太丑了,使用AsyncIterator便会直观许多:
var beginMove2 = function (e, startPos, endPos, duration, callback) {
var generator = function (ai) {
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;
beginSleep(50, ai.callback());
yield 0;
}
e.style.left = endPos.x;
e.style.top = endPos.y;
};
AsyncIterator.beginInvoke(generator, callback);
}
调用beginSleep之后代码使用yield将控制权交还给了AsyncIterator,而beginSleep完成之后也会通知AsyncIterator并继续这段逻辑。有了yield,我们的代码编写起来便顺畅多了。
示例2:批量请求数据
如果现在有一个urls数组,其中包含了目标地址,您如何将它们的结果也通过一个数组返回过来呢?伪代码如下:
// Pseudocode, cannot work
var receiveMany = function (urls) {
var results = [];
for (var i = 0; i < urls.length; i++) {
var req = new XMLHttpRequest();
req.open("GET", urls[i]);
var r = req.receive(); // cannot recieve
results.push(r);
}
return results;
}
很显然实际可以运行的代码只能是异步的:
var beginReceiveMany = function (urls, callback) {
var results = [];
var loop = function (i) {
if (i < urls.length) {
var req = new XMLHttpRequest();
req.open("GET", urls[i]);
req.beginReceive(function (r) {
results.push(r);
loop(i + 1);
});
} else {
callback(results);
}
}
loop(0);
}
之前我们扩展了XMLHttpRequest,提供了一个beginReceive方法。于是我们在回调中驰骋,最终实现了beginReceiveMany方法。如果用AsyncIterator则会方便一些,只可惜它对“带有返回值”的异步操作支持并不友好,因此还是有点绕:
var beginReceiveMany2 = function (urls, callback) {
var results = [];
var generator = function(ai) {
for (var i = 0; i < urls.length; i++) {
var req = new XMLHttpRequest();
req.open("GET", urls[i]);
req.beginReceive(ai.callback());
yield 0;
results.push(ai.result());
}
}
AsyncIterator.beginInvoke(generator, function() {
callback(results);
});
}
幸好有JavaScript的闭包在,总体来说还算方便。在这个例子中,AsyncIterator会得到beginReceive从回调函数中提交上来的结果,然后代码便可以从result方法里获得并保存。
更多
AsyncIterator虽好,只可惜只能在FireFox里使用,这自然无法推广。幸好我们还可以用Jscex,而且这一切都没有Jscex来的简单,正如之前所提到的那样,在Jscex中一段移动HTML元素的动画只需这样写:
var moveAsync = eval(Jscex.compile("$async", 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;
$await(Jscex.Async.sleep(50));
}
e.style.left = endPos.x;
e.style.top = endPos.y;
}));
批量请求数据也是最为直观的代码:
var getMultiContentAsync = eval(Jscex.compile("$async", function (urls) {
var result = [];
for (var i = 0; i < urls.length; i++) {
var content = $await(getContentAsync(urls[i]));
result.push(content);
}
return result;
}));
甚至汉诺塔的解法,也完全是最最直观的递归算法:
var hanoiAsync = eval(Jscex.compile("$async", function (n, a, b, c) {
if (n > 0) {
$await(hanoiAsync(n - 1, a, c, b));
}
$await(moveDishAsync(n, a, b));
if (n > 0) {
$await(hanoiAsync(n - 1, c, b, a));
}
}));
关于异步编程模型的演变,以及Jscex的更多内幕,本周三下午4点我将在创新院内部开展一次分享会,欢迎创新院外的朋友前来一起交流。这也是我在即将举办的SD 2.0大会上的一次预演。
扫一扫订阅我的微信号:IT技术博客大学习
- 作者:老赵 来源: 老赵点滴
- 标签: AsyncEnumerator
- 发布时间:2011-02-14 22:37:28
-
[879] WordPress插件开发 -- 在插件使用 -
[136] 解决 nginx 反向代理网页首尾出现神秘字 -
[57] 整理了一份招PHP高级工程师的面试题 -
[55] 如何保证一个程序在单台服务器上只有唯一实例( -
[55] 用 Jquery 模拟 select -
[55] 分享一个JQUERY颜色选择插件 -
[54] Innodb分表太多或者表分区太多,会导致内 -
[52] jQuery性能优化指南 -
[52] CloudSMS:免费匿名的云短信 -
[51] 海量小文件存储