资讯专栏INFORMATION COLUMN

js 处理异步操作的几种方式

Meils / 2985人阅读

摘要:如果我们只有一个异步操作,用回调函数来处理是完全没有任何问题的。事件监听使用事件监听的方式番禺广州上述代码需要实现一个事件监听器。只处理对象广州番禺函数将函数的自动执行器,改在语言层面提供,不暴露给用户。

概论

由于 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://www.ucloud.cn/yun/103044.html

相关文章

  • JS常用几种异步流程控制

    摘要:虽然这个模式运行效果很不错,但是如果嵌套了太多的回调函数,就会陷入回调地狱。当需要跟踪多个回调函数的时候,回调函数的局限性就体现出来了,非常好的改进了这些情况。 JavaScript引擎是基于单线程 (Single-threaded) 事件循环的概念构建的,同一时刻只允许一个代码块在执行,所以需要跟踪即将运行的代码,那些代码被放在一个任务队列 (job queue) 中,每当一段代码准...

    Barry_Ng 评论0 收藏0
  • 异步读取文件几种姿势

    摘要:臆想的针对读取到的内容进行操作,比如打印文件内容臆想中,读取文件是有返回值的,将返回值,即文件内容,赋给一个变量,然后决定对读取到的内容进行相应的操作,例如打印文件中的内容。 臆想的 let fs = require(fs) function readFile(filename){ ... } let content = readFile(config.js) // 针对读...

    chinafgj 评论0 收藏0
  • 关于react-router几种配置方式

    摘要:本文给大家介绍的是相比于其他框架更灵活的配置方式,大家可以根据自己的项目需要选择合适的方式。标签的方式下面我们看一个例子当为时渲染我们可以看到这种路由配置方式使用标签,然后根据找到对应的映射。 路由的概念 路由的作用就是将url和函数进行映射,在单页面应用中路由是必不可少的部分,路由配置就是一组指令,用来告诉router如何匹配url,以及对应的函数映射,即执行对应的代码。 react...

    刘永祥 评论0 收藏0
  • 50道JavaScript基础面试题(附答案)

    摘要:事件中属性等于。响应的状态为或者。同步在上会产生页面假死的问题。表示声明的变量未初始化,转换为数值时为。但并非所有浏览器都支持事件捕获。它由两部分构成函数,以及创建该函数的环境。 1 介绍JavaScript的基本数据类型Number、String 、Boolean 、Null、Undefined Object 是 JavaScript 中所有对象的父对象数据封装类对象:Object、...

    huaixiaoz 评论0 收藏0
  • JavaScript知识点总结

    摘要:参与任何数值计算的结构都是,而且。。面向人类的理性事物,而不是机器信号。达到无刷新效果。的工作原理总是指向一个对象,具体是运行时基于函数的执行环境动态绑定的,而非函数被声明时的环境。原型对象上有一个属性,该属性指向的就是构造函数。 1.JS面向对象的理解 面向对象的三大特点:继承、封装、多态 1、JS中通过prototype实现原型继承 2、JS对象可以通过对象冒充,实现多重继承, 3...

    sean 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<