适合JavaScript 1.7中迭代生成器的异步编程机制
上篇文章我提出了一种基于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 + "
");
}
}
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引擎所接受。实在可以说是委员会设计模式的又一次伟大胜利――他们就是不希望开发人员的生活能够好过一些。
建议继续学习:
- 关于IO的同步,异步,阻塞,非阻塞 (阅读:15784)
- fsockopen 异步处理 (阅读:9847)
- 腾讯敏捷开发及快速迭代 (阅读:7381)
- 配合jquery实现异步加载页面元素 (阅读:5941)
- 使用django+celery+RabbitMQ实现异步执行 (阅读:5709)
- 循环、迭代、遍历和递归 (阅读:5101)
- 多核与异步并行 (阅读:4634)
- 异步编程与响应式框架 (阅读:4522)
- Google Analytics 异步代码详解 (阅读:3922)
- redis源代码分析 - event library (阅读:3779)
扫一扫订阅我的微信号:IT技术博客大学习
- 作者:老赵 来源: 老赵点滴
- 标签: 异步 迭代
- 发布时间:2011-02-14 22:37:47
-
[882] WordPress插件开发 -- 在插件使用 -
[136] 解决 nginx 反向代理网页首尾出现神秘字 -
[57] 整理了一份招PHP高级工程师的面试题 -
[55] 用 Jquery 模拟 select -
[54] Innodb分表太多或者表分区太多,会导致内 -
[54] 分享一个JQUERY颜色选择插件 -
[54] 如何保证一个程序在单台服务器上只有唯一实例( -
[52] CloudSMS:免费匿名的云短信 -
[52] jQuery性能优化指南 -
[51] 海量小文件存储
