JavaScript版本的AsyncEnumerator

标签: 语言编程 并行处理 前端表现 | 发表时间:2010-11-30 01:18 | 作者:[email protected] (老赵) Rady
出处:http://blog.zhaojie.me/

地球人都知道,在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的原理和使用方式可以用以下几句话描述清楚:

  1. 异步代码将异步操作的结果提交至AsyncIterator提供的回调函数中(通过调用callback方法获得)。
  2. 异步代码在发起一个异步操作之后,使用yield将控制权交还给外部(其实就是AsyncIterator)。
  3. 异步操作完成后会执行AsyncIterator提供的回调函数,AsyncIterator则调用迭代器的next方法,继续执行代码。
  4. 异步代码可以从AsyncIterator的result方法中获得上一个异步操作的结果。
  5. 当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大会上的一次预演。

相关 [javascript 版本 asyncenumerator] 推荐:

JavaScript版本的AsyncEnumerator

- Rady - 老赵点滴 - 追求编程之美
地球人都知道,在C# 2.0里提供了yield关键字,可以方便好用地生成一个迭代器,更可以简化异步操作——这是因为有了Jeffrey Richter开发的AsyncEnumerator. 在接下来的某些演讲中我准备的主题是“异步编程模型”的演变,自然少不了这非常重要的一环. 为了便于广大人民群众更好地接受,我决定使用JavaScript来进行说明.

Javascript诞生记

- Milido - 阮一峰的网络日志
二周前,我谈了一点Javascript的历史. 今天把这部分补全,从历史的角度,说明Javascript到底是如何设计出来的. 只有了解这段历史,才能明白Javascript为什么是现在的样子. 我依据的资料,主要是Brendan Eich的自述. "1994年,网景公司(Netscape)发布了Navigator浏览器0.9版.

JavaScript,你懂的

- dylan - keakon的涂鸦馆
经常有人问我,JavaScript应该怎么学. 先学基本语法,如果曾学过C等语言,应该1小时内就能掌握了. 再去使用内置的函数、方法和DOM API,熟悉它能干什么;而在学习DOM API的过程中,你还不得不与HTML和CSS打交道. 然后弄懂匿名函数和闭包,学会至少一个常用的JavaScript库(例如jQuery).

Javascript 里跑Linux

- rockmaple - Shellex&#39;s Blog
牛逼到暴的大拿 Fabrice Bellard,用Javascript实现了一个x86 PC 模拟器,然后成功在这个模拟器里面跑Linux(请用Firefox 4 / Google Chrome 11打开,Chome 12有BUG). 关于这个东西… 伊说 “I did it for fun“,大大啊大大啊….

高效 JavaScript

- xtps - ITeye论坛最新讨论
传统上,网页中不会有大量的脚本,至少脚本很少会影响网页的性能. 但随着网页越来越像 Web 应用程序,脚本的效率对网页性能影响越来越大. 而且使用 Web 技术开发的应用程序现在越来越多,因此提高脚本的性能变得很重要. 对于桌面应用程序,通常使用编译器将源代码转换为二进制程序. 编译器可以花费大量时间优化最终二进制程序的效率.

你得学JavaScript

- 蒋冰 - 伯乐在线 -博客
  注:本文由 敏捷翻译 - 蒋少雄 翻译自 Kenny Meyers 的博文.   如果三年前你问我应该学什么语言,我会告诉你是Ruby.   如果你现在想学一门语言的话,你应该学习JavaScript..   我认为,每一位Web开发人员都应该学习JavaScript. 目前推出的许多新技术都支持这个观点.

javascript 贪食蛇

- Xin - 博客园-首页原创精华区
我的程序用javascript与Html中的table结合,实现的简单的贪食蛇游戏,游戏的主要特点,可调整蛇移动速度,可调整蛇移动范围,碰壁、咬到身体则“Game Over. 游戏并不完善,只是实现了主要的功用,有设计不合理的地方,欢迎您感大家提意见.        实现方法:由javascript语言中的setInterval方法驱动整个游戏程序,设置“nowDirection”即蛇的当前移动方向为全局变量,由setInterval方法定时获取蛇的移动方向,由document.onkeydown()捕捉当前按键(上、下、左、右)以修改nowDirection,这样就可以用方向按键控制蛇周期时间的定向移动.

你不懂Javascript

- 英建 - 黑客志
过去几年我注意到技术圈一个很奇怪的现象,有太多程序员将那些他们只是有过非常浅显的了解,但其实根本就不懂的技术写到他们的简历中,这个现象几乎每种语言都有,但这其中最严重的就要数Javascript了. 出现这种状况的一个很大的原因就是现如今几乎每个开发者的工作都或多或少要依赖于Javascript,但大多数人并不真的理解这门语言,他们常用的学习方式是复制粘贴,使用这种方式,你永远不会真正学会这门语言,而只能得到一个你已经懂了的假象.

Javascript 中的 var

- - 酷壳 - CoolShell.cn
MelonCard发布了一篇文章——” how one missing var ruined our launch“(”少写了一个var毁了我的网站”),这篇文章是说MelonCard用Node.js做后台,因为出了一个小高峰——有50-100人注册,结果整个网站都不响应了,而且还出现了很多奇怪的问题.

JavaScript 基础

- - CSDN博客推荐文章
JavaScript可以出现在HTML的任意地方,甚至在之前插入也不成问题,使用标签. 不过要在框架网页中插入的话,就一定要在之前插入,否则不会运行. JavaScript对数据类型要求不严格,一般来说,声明变量的时候不需要声明类型,而且就算声明了类型,在使用过程中也可以给变量赋予其他类型的值,声明变量使用var关键字.