js 处置惩罚异步操纵的几种体式格局

概论

由于 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…

    原文作者:宗介
    原文地址: https://segmentfault.com/a/1190000018670042
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞