Hello World
Spiga

适合JavaScript 1.7中迭代生成器的异步编程机制

2010-12-03 19:09 by 老赵, 2257 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引擎所接受。实在可以说是委员会设计模式的又一次伟大胜利——他们就是不希望开发人员的生活能够好过一些。

Creative Commons License

本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名赵劼(包含链接),具体操作方式可参考此处。如您有任何疑问或者授权方面的协商,请给我留言

Add your comment

11 条回复

  1. Belleve Invis
    60.166.106.*
    链接

    Belleve Invis 2010-12-03 19:55:41

    所以我才做了 lofn 的 yield 功能啊…… 是编译时膨胀展开代码。

  2. Belleve Invis
    60.166.106.*
    链接

    Belleve Invis 2010-12-03 20:00:07

    不过考虑到大多数程序猿都是按行数给工资的话,不加 yield 才能“改善生活”

  3. 老赵
    admin
    链接

    老赵 2010-12-03 22:28:57

    @Belleve Invis

    可惜用yield支持异步还是种不太完美的workaround,对于有返回值的异步方法就不太好看了。

  4. winter
    114.80.133.*
    链接

    winter 2010-12-05 16:33:43

    其实ES4委员会设计倾向更严重……ES5只是少引入了一点好的特性 ES4有点引入的太多让我受不了了。

    顺便应老赵的要求补充一下JS和ES的关系:

    JavaScript:Mozilla家的ECMAScript实现,但是很难说的一点是,这个实现比标准出现的要早一些

    • JavaScript 1.0是对ECMAScript v3的实现
    • JavaScript 1.X是一些很小的扩展,它们随着各个版本的FireFox发布
    • JavaScript 2.0是对ECMAScript v4的实现,但是它还没出生就被咔嚓掉了
    • JavaScript有时(其实大多数时候)也泛指包括ActionScript 2和JScript的这门语言

    ECMAScript:

    • ECMAScript v3就是现在绝大多数浏览器使用的语言,只不过似乎标准的名称不是很受欢迎
    • ECMAScript v4是JS的爸爸BE大人搞出来的,AS3和JavaScript2.0是对它的实现(貌似不是很严格的实现),因为引入了过多特性比如class,后被MS和Yahoo等公司给推翻了
    • ECMAScript v5的项目名为harmony,好吧你应该熟悉这个词的……ES5成功地将ES4给harmony掉了,它的改动相对保守,但现在没有被完全实现,最接近的是Chrome的JS引擎V8
  5. 链接

    小城故事 2010-12-06 09:26:09

    从JavaScript创始人对HTML5的态度就想得到,JavaScript已经难有作为了

  6. 链接

    陈梓瀚(vczh) 2010-12-06 15:39:17

    好和谐的harmony……

  7. 羊绒衫
    60.177.156.*
    链接

    羊绒衫 2010-12-06 21:58:29

    什么是同步 什么是异步,我是新手,不要见笑

  8. woxf
    221.12.174.*
    链接

    woxf 2010-12-09 09:31:14

    在读学生请教:写过两个小的应用程序,现在想转向WEB编程,能否推荐两本WEB编程方面的书。

  9. hax
    121.76.103.*
    链接

    hax 2010-12-20 01:26:46

    @小城故事

    DC不是JS的创始人,JS的创始人是BE。

    @winter

    js 1.0出现的时候,ecmascript标准还不存在。js 1.5才是es3的实现。从这个意义上说ms自称jscript是第一个实现ecmascript的。。。

    另外,BE老是受到一些不白之冤。比如虽然BE是标准委员会成员,但是es标准其实不是由他执笔的。es4的主要设计者不是BE,不过也是moz公司的人。当初的es4草案有as3和jscript.net两个实现(当然他们都又发展了一通,和moz的es4草案其实很不一样)。后来以AS3为蓝本的es4重启以后,BE是参与了设计。无论如何,结果我们都看到了,DC虎假ms威,moz被迫妥协,adobe悲剧了。

  10. AriesDevil
    113.235.123.*
    链接

    AriesDevil 2014-01-24 16:44:15

    刚看到这篇,好在 ES6 有了,早该有了

  11. 东东
    125.71.191.*
    链接

    东东 2014-02-15 20:00:55

    语义不 及 yield return

发表回复

登录 / 登录并记住我 ,登陆后便可删除或修改已发表的评论 (请注意保留评论内容)

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

评论内容(大于5个字符):

  1. Your Name yyyy-MM-dd HH:mm:ss

使用Live Messenger联系我