我所知道的JavaScript异步编程

发表于 2016-11-13
更新于 2024-05-23
分类于 技术专栏
阅读量 10990
字数统计 10365

前言

没有搞定异步编程的JS开发者不是称职的开发者

入门JS算是一年了,从当时直接使用回调到后来开始大量使用async库,期间冒出的promisegenerator都完全没有去管它。然后然后最近就被鄙视了一番(哭泣。。。。)。所以趁着刚被人鄙视完,奋发图强,好好把Js的异步编程学起来,以后去鄙视别人(哈哈哈。。。。)。

这篇文章将以代码为主,辅以解释,demo代码地址:js-async-program-demo

demo介绍

该demo是基于Express服务器框架,然后异步编程都是体现在服务器端。我们在route/index.js中处理来自客户端的请求,然后在后台中去异步请求一些信息,之后返回这些信息。然后我将使用目前JS所有原生的异步编程方式来实现这个目的,从代码中也许可以找到你想学习的信息。

在最后一个版本中使用到了async/await,但是因为Nodejs不支持,所以需要Babel的转译,于是便引进了babel-register来实时编译,更多关于babel的学习可以参考Babel6的学习新姿势

1、callback

首先是最古老最原始的异步方式-回调。作为目前最传统的方式,这种异步编程方式仍存在于大家的代码中,毕竟太过经典。但是为了更加优雅地写出漂亮的异步代码,为了展示你高逼格的代码,请在以后的异步编程中弃用它吧,否则你就可能写出demo中类似的代码:

router.get('/', function(req, res, next) {
  const finalRes = res;
  request('https://api.douban.com/v2/user/linxiaowu', (err, res, body) => {
    request('https://api.douban.com/v2/user/linxiaowu', (err, res, body) => {
      request('https://api.douban.com/v2/user/linxiaowu', (err, res, body) => {
        request('https://api.douban.com/v2/user/linxiaowu', (err, res, body) => {
          request('https://api.douban.com/v2/user/linxiaowu', (err, res, body) => {
            return finalRes.render('index', {title: 'express', result: body});
          });
        });
      });
    });
  });
});

所以传统的回调方式缺点有:

  1. 回调地狱,也就是层层嵌套,难以书写出漂亮的代码
  2. 难以调试追踪
  3. 难以处理某一个回调错误

2、promise

2.1、简短的历史

promise这个概念并不是JS特有的,很早之前C++就有这个概念,之后Python也有了。JS第一次出现Promise的概念可以追溯到2007年,在一个JS库里-MochiKit。然后Dojo引用了它,之后jquery也开始使用了。然后CommonJS组织提出了Promises/A+规范。在其早期形成时,NodeJs开始使用它了。如今promises已经成为了ES6的标准,V8引擎早已经原生地实现了。

2.2、概念

顾名思义,它的中文翻译是承诺,那就代表它会在当前或者未来的某一时刻做出它承诺的事情来,至于做出什么来便是取决于你给其提供的resolvedrejected

MDN中的定义是:

**Promise**是一个代理一个不需要知道值的对象。它允许你去关联操作到一个异步操作上的成功或者失败行为。这样可以像同步操作那样返回值,不过返回的值依然是一个promise对象,而且是在未来的某个时刻。

Promise是一个对象,其对象结构如图:

Promise的三大状态就不细说了,我们可以借助浏览器大致看一下其三大状态的变化:(记住是不可逆的)

代码如下:

状态变化如下:

在一个promise中pending之后的状态要么是resolved,要么rejected。当任意一个条件满足的时候,通过调用then方法可以将相关操作移出队列并执行。此时即使你不调用then方法的话,其结果都会存在而不会消失(除非你destroy了),好比是promise帮你暂存了信息一样。

2.3、实例演示

在demo中,我们改写之前的回调写法,使用promise来实现异步获取URL:

router.get('/promise', function(req, res, next) {
  let reqApi = new Promise((resolve, reject) => {
    request('https://api.douban.com/v2/user/linxiaowu', (err, res, body) => {
      if (err){
        reject(err);
      } else{
        resolve(body);
      }
    });
  });
  reqApi
  .then((body) => reqApi)
  .then((body) => reqApi)
  .then((body) => reqApi)
  .then((body) => reqApi)
  .then((body) => {
    return res.render('index', {title: 'express', result: body});
  })
  .catch(err => {
    console.log(err)
  })
});

我们异步多次请求URL,不过这样写的话算是“异步中的同步”,因为下一个URL获取是建立在上一个URL获取的前提下。当这些请求无关的时候我们可以使用promise.all方法:

router.get('/promiseAll', function(req, res, next) {
  let reqApi = new Promise((resolve, reject) => {
    request('https://api.douban.com/v2/user/linxiaowu', (err, res, body) => {
      if (err){
        reject(err);
      } else{
        resolve(body);
      }
    });
  });
  Promise.all([reqApi, reqApi, reqApi, reqApi, reqApi])
  .then((body) => {
    return res.render('index', {title: 'express', result: body});
  })
  .catch(err => {
    console.log(err)
  })
});

这个方法是让所有的promise都并行进行,然后如果所有的promise都被resolved了的话才返回resolved,如果只要任意一个rejected,那么就会立即返回rejected。成功的话按照给定参数的列表顺序返回对应的结果数组。

Promise里还有一个API叫做Promise.race,顾名思义就是竞争赛跑的意思,也就是说只要在所有的promise中有一个resolved或者rejected,那么就立刻返回,所以有点争上游的意思。可以参考这段代码理解一下:

router.get('/promiseRace', function(req, res, next) {
  let reqApi = new Promise((resolve, reject) => {
    request('https://api.douban.com/v2/user/linxiaowu', (err, res, body) => {
      if (err){
        reject(err);
      } else{
        resolve(body);
      }
    });
  });
  Promise.race([reqApi, reqApi, reqApi, reqApi, reqApi])
  .then((body) => {
    return res.render('index', {title: 'express', result: body});
  })
  .catch(err => {
    console.log(err)
  })
});

2.4、总结

promise总体上对于异步编程的友好性来说更上一层楼的,可以更好地控制我们的代码流。但是事物是有两面性的,Promise依然有一些缺点:

  1. 代码冗余,原来的任务被Promise 包装了一下,不管什么操作,一眼看去都是一堆 then,原来的语义变得很不清楚
  2. 当异步变多的时候会有很多的then 方法,整块代码会充满Promise 的方法,而不是业务逻辑本身
  3. 每一个then 方法内部是一个独立的作用域,要是想共享数据,就要将部分数据暴露在最外层,在then 内部赋值一次。

3、Generator

与Promise一起作为ES6标准出来的还有Generator。generator的设计允许对你隐藏异步的实现细节然后提供给你单线程,类同步风格的代码。这可以让我们以非常自然的方式表达我们程序的步骤。

3.1、概念

定义一个generator函数可以返回一个generator对象g(假设是这个名称),该对象遵循了迭代协议迭代器协议。换句话说就是可以使用Array.from(g), [...g]for value of g来进行迭代循环。

Generator函数允许你声明一个特殊类型的迭代器,每一次迭代就代表你的代码的一次休眠(也就是将执行权交还)。

generator函数的定义都必须在function后面加个*以示区别,比如:

此时g的对象结构如图:

Tips

Symbol.iterator:这个也是ES6的新特性,简单说这个相当于迭代器的接口,只有对象里有这个symbol的属性,才可以认为此对象是可迭代的。只要一个对象实现了正确的 Symbol.iterator 方法,那么它就可以被 for in 所遍历。

@@iterator:等价于Symbol.iterator

.next()每次都返回一个done属性和value值,前者指示是否已经迭代完成,后者指示当前结果。

如图:

这里注意有两个结果需要区分:一个是next()执行的结果另外一个是yield表达式返回的结果。在后面的例子中我们会细说一下区别。

另外值得注意的是上下文在休眠和恢复的时候都会被保存着,这意味着generators是有状态的。

3.2、如何使用它来实现异步编程

那么根据generator的使用方法应该来设计一个异步操作呢?根据异步的返回类型我们设计出两个demo:

3.2.1、callback版本的generator

router.get('/generator', function(req, res, next) {
  const finalRes = res;
  function* generator(){
    let val = [];
    function request_g(url){
      request(url, (err, res, body) => {
        if (err) {
          g.throw(err)
        }
        g.next(body);
      });
    }
    val[0] = yield request_g('https://api.douban.com/v2/user/linxiaowu');
    val[1] = yield request_g('https://api.douban.com/v2/user/linxiaowu');
    val[2] = yield request_g('https://api.douban.com/v2/user/linxiaowu');
    val[3] = yield request_g('https://api.douban.com/v2/user/linxiaowu');
    val[4] = yield request_g('https://api.douban.com/v2/user/linxiaowu');
    return finalRes.render('index', {title: 'express', result: val});
  }
  var g = generator();
  g.next(); // ---return is { value: undefined, done: false }
})

request_g帮助我们封装了request这个异步操作,确保它在回调的时候可以调用到generator的迭代器next()方法。

3.2.1.1、异步流程解析
  1. 首先我们获得generator对象g,然后执行.next()的方法,它就开始执行generator函数里面的语句直到遇到了第一个yield表达式
  2. 然后开始执行request_g这个函数并立即返回undefined值给next,于是如我们代码注释的g.next()返回得到的东西是request_g执行的结果。
  3. 休眠generator函数,执行权交还给CPU。
  4. 在某个时间点request请求成功执行回调,于是在回调中就发现有.next方法,接着唤醒generator,并且带着request请求回来的body参数赋值给了val[0]
  5. 接着从刚开始暂停的地方继续执行,同样的过程再继续重复,直到最后一个yield表达式。

可以看到它通过隐藏在yield中的pause的能力,然后将generator的resume能力分离到另外一个函数(request_g)来实现了异步,这样我们的代码就看起来就像是同步一样了。

上面的写法对于简单的异步操作还能够胜任,可是遇到复杂的异步就不行不行的,这个时候我们就需要更加强大的机制来使用generator,于是就有下面的版本Promise + generator。

3.2.2、promise版本的generator

我们可以使用刚才的request来封装一个promise对象,也可以使用现成的request-promise

前者可以是:

function request-p(url) {
    // Note: returning a promise now!
    return new Promise( function(resolve,reject){
        request( url, resolve );
    } );
}

后者使用request-promise,如下:

function *generator(){
  yield request_promise({
    url: 'https://api.github.com/repos/linxiaowu66/react-table-demo',
    method: 'get',
    headers: {
      'User-Agent': 'request'
    }
  })

  /*you can Add more Async operation*/
}
router.get('/generator-promise', function(req, res, next) {
  const g = generator();
  let val = [];
  (function iterator(){
    let next = g.next();
    if (next.done){
      return res.render('index', {title: 'express', result: val});
    }
    next.value.then( data => {
      val.push(data);
      iterator();
    }).catch( err => {
      console.log(err);
      return res.render('index', {title: 'express', result: err});
    })
  })()

})

3.2.2.1、异步流程解析

以后者的实现来说:

  1. 首先我们获得generator对象g,然后执行一个立即执行函数--iterator()
  2. 立即函数内部执行.next()操作,它便开始执行generator的第一个yield表达式,该表达式直接返回一个promise对象给next.value
  3. 休眠generator函数,执行权交还给CPU。
  4. CPU继续执行,判断返回的next.done是否为true,如果是的话表明所有的异步操作已经完成,于是返回网页
  5. 为返回的promise注册resolvedrejected的操作;
  6. 在未来的某个时间点,request请求成功返回,调用.then的操作,保存结果然后递归调用iterator()。请求失败的话调用.catch的操作,返回网页并显示错误消息。

结论

可以看到,虽然Generator函数将异步操作表示得很简洁,但是流程管理却不方便(即何时执行第一阶段、何时执行第二阶段)

4、Async/Aswait

JavaScript不断地演进,到了ES7又有了新的异步编程方式,就好比class是原型的语法糖一样,Async/Await也是为了提供给开发者一个更加简单、清楚的语法(不需要你像generator那样不断地使用.next控制执行过程,而是全都自动化,只需要你定义好async函数以及await表达式)。因为简单,所以我们一目了然地就能看懂整个代码。立马上code:

async function getUrl (){
  let reqApi = new Promise((resolve, reject) => {
    request('https://api.douban.com/v2/user/linxiaowu', (err, res, body) => {
      if (err){
        reject(err);
      } else{
        resolve(body);
      }
    });
  });
  try{
    let result = await Promise.all([reqApi, reqApi, reqApi, reqApi]);
    return result;
  } catch(err){
    console.log(err)
  }
}
router.get('/async', function(req, res, next) {
  getUrl().then(data => {
    res.render('index', {title: 'express', result: data});
  })
  .catch(err => {
    res.render('index', {title: 'express', result: err});
  });
});

Tips:

  1. 一个Async函数总是返回一个Promise,该Promise如果是捕捉到错误的时候是rejected,否则总是resolved。这样的话我们就可以调用一个async函数并且将其结合到我们正常使用的Promise的链式调用中

  2. 我们在使用await的时候一般都是认为“等待”promise,但并没有真的等待哈。很多人其实都不知道async/await的整个基础其实就是promise。实际上你写的每一个async函数都会返回一个promise,你每一个单独等待的函数都会是一个promise的(如果awaited函数不是promise,它会强转为promise格式)。

参考:

  1. https://ponyfoo.com/articles/understanding-javascript-async-await
  2. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Generator
  3. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/iterator

公众号关注一波~

微信公众号

关于评论和留言

如果对本文 我所知道的JavaScript异步编程 的内容有疑问,请在下面的评论系统中留言,谢谢。

网站源码:linxiaowu66 · 豆米的博客

Follow:linxiaowu66 · Github