异步对语言来说绝对是一个重要的地位,不管是前端为了用户体验,或者是后端为了吞吐量。正因为如此,如何快速或优美实现异步变成了更重要的事。

一、概念

1、JavaScript 异步

JavaScript中所谓的异步是指:将某个任务拆分两段,分别执行。

2、Event Loop

由于负责JavaScript执行的只是一个单线程,当我们访问一个远程Http时这是需要一个时间等待,而不能因为这种等待让浏览器处于假死。所以就有了Event Loop。

简单说程序实现一个线程来负责JavaScript执行,而Event Loop是负责JavaScript线程体和程序的其他进程(如:I/O操作、Http等等)的通信。

二、实现方式

为了方便,每种实现方式都假定引用jQuery,且有以下配置信息。

var cog = {
    url: 'file.txt'
}

1、回调函数

即前面说到异步的第二段代码,就叫“回调函数”,洋文名叫callback,比如:

$.get(cog.url, function(data){
    console.log(data);
});

get 的第二个参数 function 就叫回调函数,$.get 在请求http返回结果后才会执行该函数。

2、Promise 对象

回调函数的确看起来也非常的简便,使用起来并没有什么问题;可必须依赖第一段代码,且当我们要请求两个远程信息时,就会变成这样子:

$.get(cog.url, function(data){
    console.log(data);
    $.get(cog.url, function(data){
        console.log(data);
    });
});

反正我是受不了。而使用 Promise 变成这样:

$.get(cog.url)
    .then(function(data) {
        console.log('f1:' + data);
    })
    .then(function() {
        return $.get(cog.url);
    })
    .then(function(data) {
        console.log('f2:' + data);
    });

变成了好多 then 的写法,但至少解决两个问题:依赖和变态的嵌套。

Promise 只是一种实现的思路,所以各家在实现上也相对于比较统一接口,比如上面的then。好在ES6版本中,对 Promise 的实现统一了接口。前面是猜jQuery的Promise对象,隐藏了细节,我们来试试ES6的原生写法。

function ajax(url) {
    return new Promise(function(resolve, reject) {
        let xhr = new XMLHttpRequest();
        xhr.onreadystatechange = function() {
            if (this.readyState === 4) {
                if (this.status === 200)
                    resolve(this.response);
                else
                    reject(new Error(this.status));
            }
        };
        xhr.open("GET", url, true);
        xhr.send();
    })
}

ajax(cog.url)
    .then(function(data) {
        console.log('f1:' + data);
    })
    .then(function() {
        return ajax(cog.url);
    })
    .then(function(data) {
        console.log('f2:' + data);
    });

ajax 函数里面只是实例一个并返回 new Promise(),且接受两个参数:resolve(解决)和reject(拒绝)函数。而内部分别是对AJAX请求成功和失败的处理。

可见在 ES6 当中已经简化了 Promise 的实现成本。

然而和回调函数相比,到处的 then 也挺烦人。

3、Generator 函数

在说之前,我们先来认识一下 yield 关键词。说到这个,懂C#的人会很清楚,因为在C#很早就有这个,用于迭代器的延迟加载,可避免一次性加载所有可能并不能完全有用的数据。

Generator只不过一种名词叫法,而在传统编程当中为了解决异步问题,提出一种叫“协程”的东西,做法简单来说就是:函数A的线程执行到一半,然后暂停遇到IO要等待,就把这个执行权交到别的函数B上,然后过段时间回来看一下A,如果处于等待那么就把执行权交还给函数A的线程。

而说白了,ES6的Generator就是协程的一个实现。但感觉好像好高端上的样子,真正的只有一个关键词 yield

先来看一下示例:

function ajax(url) {
    let xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function() {
        if (this.readyState === 4 && this.status === 200) {
            it.next(this.response);
        }
    };
    xhr.open("GET", url, true);
    xhr.send();
}

function* main() {
    let f1 = yield ajax(cog.url);
    console.log(f1);
    let f2 = yield ajax(cog.url);
    console.log(f2);
}

const it = main();
it.next();

抛开一些细节,光看 main() 函数的代码,是不是很爽呢?

首先:function 紧接着一个星号。

其次:我们来分析一下执行的流程。

  • 实例main()后,随后调用 next()
  • 执行到第一个 yield,接着进入ajax(),直到等到下一个next()出现,才会返回到main()函数体。
  • ajax请求结束直到遇到 it.next(this.response); 才会再返回main()函数体。
  • …………交替一直执行至结束…………

三、比较

1、语法

1、回调函数最简单、易理解,但很明显当多个时callback就到处飞,函数彼此之间都存在的依赖。
2、Promise相比回调函数来说函数间解耦了,但是到处then也有很多代码的冗余。但至少漂亮许多。
3、Generator作为ES6的标准存在,进一步解决了Promise冗余问题;但假如要想写好,我们还需要借助一些运行器之类来辅助,这样的代码才会更优美。但此我需要另一篇文章来解释。

2、学习曲线

1、假如明确知道函数只可能单一被使用,或者说如果函数并非是基础库,而是业务运用的次数很单一,回调函数不失是一种非常好的办法。
2、Promise在ES6之前,就已经有好多库:jQuery、when.js、Q等等提供的Promise抽象,大大的简化学习;而ES6提供的接口也极为方便。
3、Generator是一个全新的ES6接口,其实把Generator当作异步来解释我个人觉得真的很牵强,因为Generator目的更为的是迭代器优化。

3、浏览器支持现状

1、回调函数这毋庸置疑任何浏览器都没有问题。
2、Promise的第三方类库也解决了兼容性问题,而如果是ES6的写法估计在老版本的浏览器上特别是IE,是无法支持。
3、Generator同Promise的现状一样。

参考