技术头条 - 一个快速在微博传播文章的方式     搜索本站
您现在的位置首页 --> JavaScript --> 适合JavaScript 1.7中迭代生成器的异步编程机制

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

浏览:1833次  出处信息

    上篇文章我提出了一种基于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引擎所接受。实在可以说是委员会设计模式的又一次伟大胜利――他们就是不希望开发人员的生活能够好过一些。

建议继续学习:

  1. 关于IO的同步,异步,阻塞,非阻塞    (阅读:14311)
  2. fsockopen 异步处理    (阅读:8913)
  3. 腾讯敏捷开发及快速迭代    (阅读:6362)
  4. 配合jquery实现异步加载页面元素    (阅读:5300)
  5. 使用django+celery+RabbitMQ实现异步执行    (阅读:4904)
  6. 循环、迭代、遍历和递归    (阅读:4324)
  7. 异步编程与响应式框架    (阅读:3782)
  8. 多核与异步并行    (阅读:3730)
  9. redis源代码分析 - event library    (阅读:3078)
  10. Google Analytics 异步代码详解    (阅读:3032)
QQ技术交流群:445447336,欢迎加入!
扫一扫订阅我的微信号:IT技术博客大学习
© 2009 - 2024 by blogread.cn 微博:@IT技术博客大学习

京ICP备15002552号-1