1

概论

由于 JavaScript 是一门单线程执行的语言,所以在我们处理耗时较长的任务时,异步编程就显得尤为重要。
js 处理异步操作最传统的方式是回调函数,基本上所有的异步操作都可以用回调函数来处理;
为了使代码更优雅,人们又想到了用事件监听、发布/订阅模式和 Promise 等来处理异步操作;
之后在 ES2015 语言标准中终于引入了Promise,从此浏览器原生支持 Promise ;
此外,ES2015 中的生成器generator因其中断/恢复执行和传值等优秀功能也被人们用于异步处理;
之后,ES2017 语言标准又引入了更优秀的异步处理方法async/await......

异步处理方式

为了更直观地发现这些异步处理方式的优势和不足,我们将分别使用不同的方式解决同一个异步问题。
问题:假设我们需要用原生 XMLHttpRequest 获取两个 json 数据 —— 首先异步获取广州的天气,等成功后再异步获取番禺的天气,最后一起输出获取到的两个 json 数据。
前提:假设我们已经了解了Promise,generatorasync

回调函数

我们首先用最传统的回调函数来处理:

var xhr1 = new XMLHttpRequest();
xhr1.open('GET', 'https://www.apiopen.top/weatherApi?city=广州');
xhr1.send();
xhr1.onreadystatechange = function() {
    if(this.readyState !== 4)  return;
    if(this.status === 200) {
        data1 = JSON.parse(this.response);
        var xhr2 = new XMLHttpRequest();
        xhr2.open('GET', 'https://www.apiopen.top/weatherApi?city=番禺');
        xhr2.send();
        xhr2.onreadystatechange = function() {
            if(this.readyState !== 4)  return;
            if(this.status === 200) {
                data2 = JSON.parse(this.response);
                console.log(data1, data2);
            }
        }
    }
};

优点:简单、方便、实用。
缺点:易形成回调函数地狱。如果我们只有一个异步操作,用回调函数来处理是完全没有任何问题的。如果我们在回调函数中再嵌套一个回调函数,问题也不大。但是如果我们要嵌套很多个回调函数,问题就很大了,因为多个异步操作形成了强耦合,代码将乱作一团,无法管理。这种情况被称为"回调函数地狱"(callback hell)。

事件监听

使用事件监听的方式:

var events = new Events();
events.addEvent('done', function(data1) {
    var xhr = new XMLHttpRequest();
    xhr.open('GET', 'https://www.apiopen.top/weatherApi?city=番禺');
    xhr.send();
    xhr.onreadystatechange = function() {
        if(this.readyState !== 4)  return;
        if(this.status === 200) {
            data1 = JSON.parse(data1);
            var data2 = JSON.parse(this.response);
            console.log(data1, data2);
        }
    }
});

var xhr = new XMLHttpRequest();
xhr.open('GET', 'https://www.apiopen.top/weatherApi?city=广州');
xhr.send();
xhr.onreadystatechange = function() {
    if(this.readyState !== 4)  return;
    if(this.status === 200) {
        events.fireEvent('done', this.response);
    }
};

上述代码需要实现一个事件监听器 Events。
优点:与回调函数相比,事件监听方式实现了代码的解耦,将两个回调函数分离了开来,更方便进行代码的管理。
缺点:使用起来不方便,每次都要手动地绑定和触发事件。
而发布/订阅模式与其类似,就不多说了。

Promise

使用 ES6 Promise 的方式:

new Promise(function(resolve, reject) {
    const xhr = new XMLHttpRequest();
    xhr.open('GET', 'https://www.apiopen.top/weatherApi?city=广州');
    xhr.send();
    xhr.onreadystatechange = function() {
        if(this.readyState !== 4)  return;
        if(this.status === 200) return resolve(this.response);
        reject(this.statusText);
    };
}).then(function(value) {
    const xhr = new XMLHttpRequest();
    xhr.open('GET', 'https://www.apiopen.top/weatherApi?city=番禺');
    xhr.send();
    xhr.onreadystatechange = function() {
        if(this.readyState !== 4)  return;
        if(this.status === 200) {
            const data1 = JSON.parse(value);
            const data2 = JSON.parse(this.response);
            console.log(data1, data2);
        }
    };
});

优点:使用Promise的方式,我们成功地将回调函数嵌套调用变成了链式调用,与前两种方式相比逻辑更强,执行顺序更清楚。
缺点:代码冗余,异步操作都被包裹在Promise构造函数和then方法中,主体代码不明显,语义变得不清楚。

generator + 回调函数

接下来,我们使用 generator 和回调函数来实现。
首先用一个 generator function 封装异步操作的逻辑代码:

function* gen() {
    const data1 = yield getJSON_TH('https://www.apiopen.top/weatherApi?city=广州');
    const data2 = yield getJSON_TH('https://www.apiopen.top/weatherApi?city=番禺');
    console.log(data1, data2);
}

看了这段代码,是不是感觉它很直观、很优雅。实际上,除去星号和yield关键字,这段代码就变得和同步代码一样了。
当然,只有这个 gen 函数是没有用的,直接执行它只会得到一个generator对象。我们需要用它返回的 generator 对象来恢复/暂停 gen 函数的执行,同时传递数据到 gen 函数中。
getJSON_TH函数封装异步操作的主体代码:

function getJSON_TH(url) {
    return function(fn) {
        const xhr = new XMLHttpRequest();
        
        xhr.open('GET', url);
        xhr.responseType = "json";
        xhr.setRequestHeader("Accept", "application/json");
        xhr.send();
        
        xhr.onreadystatechange = function() {
            if(this.readyState !== 4)  return;
            let err, data;
            if(this.status === 200) {
                data = this.response;
            } else {
                err = new Error(this.statusText);
            }
            fn(err, data);
        }
    }
}

有的同学可能觉得直接给getJSON_TH函数传入 url 和 fn 两个参数不就行了吗,为什么非要返回一个函数。其实这正是奥妙所在,getJSON_TH函数返回的函数是一个Thunk函数,它只接收一个回调函数作为参数。通过Thunk函数或者说Thunk函数的回调函数,我们可以在 gen 函数外部向其内部传入数据,同时恢复 gen 函数的执行。在 node.js 中,我们可以通过 Thunkify 模块将带回调参数的函数转化为 Thunk 函数。
接下来,我们手动执行 gen 函数:

const g = gen();

g.next().value((err, data) => {
    if(err) return g.throw(err);
    g.next(data).value((err, data) => {
        if(err) return g.throw(err);
        g.next(data);
    })
});

其中,g.next().value 就是 gen 函数中yield输出的值,也就是我们之前提到的Thunk函数,我们在它的回调函数中,通过 g.next(data) 方法将 data 传给 gen 函数中的 data1,并且恢复 gen 函数的执行(将 gen 函数的执行上下文再次压入调用栈中)。
方便起见,我们还可以将自动执行 gen 函数的操作封装起来:

function run(gen) {
    const g =  gen();
    
    function next(err, data) {
        if(err) return g.throw(err);
        const res = g.next(data);
        if(res.done) return;
        res.value(next);
    }
    
    next();
}

run(gen);

优点:generator 方式使得异步操作很接近同步操作,十分的简洁明了。另外,gen 执行 yield 语句时,只是将执行上下文暂时弹出,并不会销毁,这使得上下文状态被保存。
缺点:流程管理不方便,需要一个执行器来执行 generator 函数。

generator + Promise

除了Thunk函数,我们还可以借助Promise对象来执行 generator 函数。
同样优雅的逻辑代码:

function* gen() {
    const data1 = yield getJSON_PM('https://www.apiopen.top/weatherApi?city=广州');
    const data2 = yield getJSON_PM('https://www.apiopen.top/weatherApi?city=番禺');
    console.log(data1, data2);
}

getJSON_PM函数返回一个 Promise 对象:

function getJSON_PM(url) {
    return new Promise((resolve, rejext) => {
        const xhr = new XMLHttpRequest();
        
        xhr.open('GET', url);
        xhr.responseType = "json";
        xhr.setRequestHeader("Accept", "application/json");
        xhr.send();
        
        xhr.onreadystatechange = function() {
            if(this.readyState !== 4) return;
            if(this.status === 200) return resolve(this.response);
            reject(new Error(this.statusText));
        };
    });
}

手动执行 generator 函数:

const g = gen();

g.next().value.then(data => {
    g.next(data).value.then(data => g.next(data), err => g.throw(err));
}, err => g.throw(err));

自动执行 generator 函数:

function run(gen) {
    const g = gen();
    
    function next(data) {
        const res = g.next(data);
        if(res.done) return;
        res.value.then(next);
    }
    
    next();
}

run(gen);

generator + co 模块

node.js 中的co模块是一个用来自动执行generator函数的模块,它的入口是一个co(gen)函数,它预期接收一个 generator 对象或者 generator 函数作为参数,返回一个Promise对象。

在参数 gen 函数中,yield语句预期接收一个 generator 对象,generator 函数,thunk 函数,Promise 对象,数组或者对象。co模块的主要实现原理是将 yield 接收的值统一转换成一个Promise对象,然后用类似上述 generator + Promise 的方法来自动执行 generator 函数。

下面是我根据 node.js co 模块源码修改的 es6 co 模块,让它更适合自己使用:
https://github.com/lyl123321/...

yield接收thunk函数:

import co from './co.mjs'

function* gen() {
    const data1 = yield getJSON_TH('https://www.apiopen.top/weatherApi?city=广州');
    const data2 = yield getJSON_TH('https://www.apiopen.top/weatherApi?city=番禺');
    console.log(data1, data2);
}

co(gen);

yield接收Promise对象:

function* gen() {
    const data1 = yield getJSON_PM('https://www.apiopen.top/weatherApi?city=广州');
    const data2 = yield getJSON_PM('https://www.apiopen.top/weatherApi?city=番禺');
    console.log(data1, data2);
}

co(gen);

async/await

async函数是generator函数的语法糖,它相对于一个自带执行器(如 co 模块)的generator函数。

async函数中的await关键字预期接收一个Promise对象,如果不是 Promise 对象则返回原值,这使得它的适用性比 co 执行器更广。

async函数返回一个Promise对象,这点与 co 执行器一样,这使得async函数比返回generator对象的generator函数更实用。如果 async 函数顺利执行完,则返回的 Promise 对象状态变为 fulfilled,且 value 值为 async 函数中 return 关键字的返回值;如果 async 函数执行时遇到错误且没有在 async 内部捕获错误,则返回的 Promise 对象状态变为 rejected,且 reason 值为 async 函数中的错误。

await只处理Promise对象:

async function azc() {
    const data1 = await getJSON_PM('https://www.apiopen.top/weatherApi?city=广州');
    const data2 = await getJSON_PM('https://www.apiopen.top/weatherApi?city=番禺');
    console.log(data1, data2);
}

azc();

async函数将generator函数的自动执行器,改在语言层面提供,不暴露给用户。

async function fn(args) {
  // ...
}

相当于:

function fn(args) {
  return exec(function* () {
    // ...
  });
}

优点:最简洁,最符合语义,最接近同步代码,最适合处理多个 Promise 异步操作。相比 generator 方式,async 方式省掉了自动执行器,减少了代码量。
缺点:js 语言自带的 async 执行器功能性可能没有 co 模块等执行器强。你可以根据自己的需求定义自己的 generator 函数执行器。

参考链接:
http://es6.ruanyifeng.com/#do...


宗介
177 声望14 粉丝