这篇文章是讲JS异步原理和实现形式的第四篇文章,后面三篇是:

setTimeout和setImmediate到底谁先执行,本文让你彻底了解Event Loop

从公布订阅模式动手读懂Node.js的EventEmitter源码

手写一个Promise/A+,完满通过官网872个测试用例

本文次要会讲Generator的使用和实现原理,而后咱们会去读一下co模块的源码,最初还会提一下async/await。

本文全副例子都在GitHub上:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/JavaScript/Generator

Generator

异步编程始终是JS的外围之一,业界也是始终在摸索不同的解决办法,从“回调天堂”到公布订阅模式,再到Promise,都是在优化异步编程。只管Promise曾经很优良了,也不会陷入“回调天堂”,然而嵌套层数多了也会有一连串的then,始终不能像同步代码那样间接往下写就行了。Generator是ES6引入的进一步改善异步编程的计划,上面咱们先来看看根本用法。

根本用法

Generator的中文翻译是“生成器”,其实他要干的事件也是一个生成器,一个函数如果加了*,他就会变成一个生成器函数,他的运行后果会返回一个迭代器对象,比方上面的代码:

// gen是一个生成器函数function* gen() {  let a = yield 1;  let b = yield a + 2;  yield b + 3;}let itor = gen();   // 生成器函数运行后会返回一个迭代器对象,即itor。

next

ES6标准中规定迭代器必须有一个next办法,这个办法会返回一个对象,这个对象具备donevalue两个属性,done示意以后迭代器内容是否曾经执行完,执行完为true,否则为falsevalue示意以后步骤返回的值。在generator具体使用中,每次遇到yield关键字都会暂停执行,当调用迭代器的next时,会将yield前面表达式的值作为返回对象的value,比方下面生成器的执行后果如下:

咱们能够看到第一次调next返回的就是第一个yeild前面表达式的值,也就是1。须要留神的是,整个迭代器目前暂停在了第一个yield这里,给变量a赋值都没执行,要调用下一个next的时候才会给变量a赋值,而后始终执行到第二个yield。那应该给a赋什么值呢?从代码来看,a的值应该是yield语句的返回值,然而yield自身是没有返回值的,或者说返回值是undefined,如果要给a赋值须要下次调next的时候手动传进去,咱们这里传一个4,4就会作为上次yield的返回值赋给a:

能够看到第二个yield前面的表达式a + 2的值是6,这是因为咱们传进去的4被作为上一个yield的返回值了,而后计算a + 2天然就是6了。

咱们持续next,把这个迭代器走完:

上图是接着后面运行的,图中第一个next返回的valueNaN是因为咱们调next的时候没有传参数,也就是说bundefinedundefined + 3就为NaN了 。最初一个next其实是把函数体执行完了,这时候的value应该是这个函数return的值,然而因为咱们没有写return,默认就是return undefined了,执行完后done会被置为true

throw

迭代器还有个办法是throw,这个办法能够在函数体内部抛出谬误,而后在函数外面捕捉,还是下面那个例子:

function* gen() {  let a = yield 1;  let b = yield a + 2;  yield b + 3;}let itor = gen();  

咱们这次不必next执行了,间接throw谬误进去:

这个谬误因为咱们没有捕捉,所以间接抛到最外层来了,咱们能够在函数体外面捕捉他,略微改下:

function* gen() {  try {    let a = yield 1;    let b = yield a + 2;    yield b + 3;  } catch (e) {    console.log(e);  }}let itor = gen();  

而后再来throw下:

这个图能够看进去,谬误在函数里外面捕捉了,走到了catch外面,这外面只有一个console同步代码,整个函数间接就运行完结了,所以done变成true了,当然catch外面能够持续写yield而后用next来执行。

return

迭代器还有个return办法,这个办法就很简略了,他会间接终止以后迭代器,将done置为true,这个办法的参数就是迭代器的value,还是下面的例子:

function* gen() {  let a = yield 1;  let b = yield a + 2;  yield b + 3;}let itor = gen();  

这次咱们间接调用return:

yield*

简略了解,yield*就是在生成器外面调用另一个生成器,然而他并不会占用一个next,而是间接进入被调用的生成器去运行。

function* gen() {  let a = yield 1;  let b = yield a + 2;}function* gen2() {  yield 10 + 5;  yield* gen();}let itor = gen2();  

下面代码咱们第一次调用next,值天然是10 + 5,即15,而后第二次调用next,其实就走到了yield*了,这其实就相当于调用了gen,而后执行他的第一个yield,值就是1。

协程

其实Generator就是实现了协程,协程是一个比线程还小的概念。一个过程能够有多个线程,一个线程能够有多个协程,然而一个线程同时只能有一个协程在运行。这个意思就是说如果以后协程能够执行,比方同步代码,那就执行他,如果以后协程临时不能继续执行,比方他是一个异步读文件的操作,那就将它挂起,而后去执行其余协程,等这个协程后果回来了,能够持续了再来执行他。yield其实就相当于将当前任务挂起了,下次调用再从这里开始。协程这个概念其实很多年前就曾经被提出来了,其余很多语言也有本人的实现。Generator相当于JS实现的协程。

异步利用

后面讲了Generator的根本用法,咱们用它来解决一个异步事件看看。我还是应用后面文章用到过的例子,三个网络申请,申请3依赖申请2的后果,申请2依赖申请1的后果,如果应用回调是这样的:

const request = require("request");request('https://www.baidu.com', function (error, response) {  if (!error && response.statusCode == 200) {    console.log('get times 1');    request('https://www.baidu.com', function(error, response) {      if (!error && response.statusCode == 200) {        console.log('get times 2');        request('https://www.baidu.com', function(error, response) {          if (!error && response.statusCode == 200) {            console.log('get times 3');          }        })      }    })  }});

咱们这次应用Generator来解决“回调天堂”:

const request = require("request");function* requestGen() {  function sendRequest(url) {    request(url, function (error, response) {      if (!error && response.statusCode == 200) {        console.log(response.body);        // 留神这里,援用了内部的迭代器itor        itor.next(response.body);      }    })  }  const url = 'https://www.baidu.com';  // 应用yield发动三个申请,每个申请胜利后再持续调next  const r1 = yield sendRequest(url);  console.log('r1', r1);  const r2 = yield sendRequest(url);  console.log('r2', r2);  const r3 = yield sendRequest(url);  console.log('r3', r3);}const itor = requestGen();// 手动调第一个nextitor.next();

这个例子中咱们在生成器外面写了一个申请办法,这个办法会去发动网络申请,每次网络申请胜利后又持续调用next执行前面的yield,最初是在外层手动调一个next触发这个流程。这其实就相似一个尾调用,这样写能够达到成果,然而在requestGen外面援用了里面的迭代器itor,耦合很高,而且不好复用。

thunk函数

为了解决后面说的耦合高,不好复用的问题,就有了thunk函数。thunk函数了解起来有点绕,我先把代码写进去,而后再一步一步来剖析它的执行程序:

function Thunk(fn) {  return function(...args) {    return function(callback) {      return fn.call(this, ...args, callback)    }  }}function run(fn) {  let gen = fn();    function next(err, data) {    let result = gen.next(data);        if(result.done) return;        result.value(next);  }    next();}// 应用thunk办法const request = require("request");const requestThunk = Thunk(request);function* requestGen() {  const url = 'https://www.baidu.com';    let r1 = yield requestThunk(url);  console.log(r1.body);    let r2 = yield requestThunk(url);  console.log(r2.body);    let r3 = yield requestThunk(url);  console.log(r3.body);}// 启动运行run(requestGen);

这段代码外面的Thunk函数返回了好几层函数,咱们从他的应用动手一层一层剥开看:

  1. requestThunk是Thunk运行的返回值,也就是第一层返回值,参数是request,也就是:

    function(...args) {  return function(callback) {    return request.call(this, ...args, callback);   // 留神这里调用的是request  }}
  2. run函数的参数是生成器,咱们看看他到底干了啥:

    1. run外面先调用生成器,拿到迭代器gen,而后自定义了一个next办法,并调用这个next办法,为了便于辨别,我这里称这个自定义的next为部分next
    2. 部分next会调用生成器的next,生成器的next其实就是yield requestThunk(url),参数是咱们传进去的url,这就调到咱们后面的那个办法,这个yield返回的value其实是:

      function(callback) {  return request.call(this, url, callback);   }
    3. 检测迭代器是否曾经迭代结束,如果没有,就持续调用第二步的这个函数,这个函数其实才真正的去request,这时候传进去的参数是部分next,部分next也作为了request的回调函数。
    4. 这个回调函数在执行时又会调gen.next,这样生成器就能够持续往下执行了,同时gen.next的参数是回调函数的data,这样,生成器外面的r1其实就拿到了申请的返回值。

Thunk函数就是这样一种能够主动执行Generator的函数,因为Thunk函数的包装,咱们在Generator外面能够像同步代码那样间接拿到yield异步代码的返回值。

co模块

co模块是一个很受欢迎的模块,他也能够主动执行Generator,他的yield前面反对thunk和Promise,咱们先来看看他的根本应用,而后再去剖析下他的源码。
官网GitHub:https://github.com/tj/co

根本应用

反对thunk

后面咱们讲了thunk函数,咱们还是从thunk函数开始。代码还是用咱们后面写的thunk函数,然而因为co反对的thunk是只接管回调函数的函数模式,咱们应用时须要调整下:

// 还是之前的thunk函数function Thunk(fn) {  return function(...args) {    return function(callback) {      return fn.call(this, ...args, callback)    }  }}// 将咱们须要的request转换成thunkconst request = require('request');const requestThunk = Thunk(request);// 转换后的requestThunk其实能够间接用了// 用法就是 requestThunk(url)(callback)// 然而咱们co接管的thunk是 fn(callback)模式// 咱们转换一下// 这时候的baiduRequest也是一个函数,url曾经传好了,他只须要一个回调函数做参数就行// 应用就是这样:baiduRequest(callback)const baiduRequest = requestThunk('https://www.baidu.com');// 引入co执行, co的参数是一个Generator// co的返回值是一个Promise,咱们能够用then拿到他的后果const co = require('co');co(function* () {  const r1 = yield baiduRequest;  const r2 = yield baiduRequest;  const r3 = yield baiduRequest;    return {    r1,    r2,    r3,  }}).then((res) => {  // then外面就能够间接拿到后面返回的{r1, r2, r3}  console.log(res);});

反对Promise

其实co官网是倡议yield前面跟Promise的,尽管反对thunk,然而将来可能会移除。应用Promise,咱们代码写起来其实更简略,间接用fetch就行,不必包装Thunk。

const fetch = require('node-fetch');const co = require('co');co(function* () {  // 间接用fetch,简略多了,fetch返回的就是Promise  const r1 = yield fetch('https://www.baidu.com');  const r2 = yield fetch('https://www.baidu.com');  const r3 = yield fetch('https://www.baidu.com');    return {    r1,    r2,    r3,  }}).then((res) => {  // 这里同样能够拿到{r1, r2, r3}  console.log(res);});

源码剖析

本文的源码剖析基于co模块4.6.0版本,源码:https://github.com/tj/co/blob/master/index.js

认真看源码会发现他代码并不多,总共两百多行,一半都是在进行yield前面的参数检测和解决,检测他是不是Promise,如果不是就转换为Promise,所以即便你yield前面传的thunk,他还是会转换成Promise解决。转换Promise的代码绝对比拟独立和简略,我这里不具体开展了,这里次要还是讲一讲外围办法co(gen)。上面是我复制的去掉了正文的简化代码:

function co(gen) {  var ctx = this;  var args = slice.call(arguments, 1);  return new Promise(function(resolve, reject) {    if (typeof gen === 'function') gen = gen.apply(ctx, args);    if (!gen || typeof gen.next !== 'function') return resolve(gen);    onFulfilled();    function onFulfilled(res) {      var ret;      try {        ret = gen.next(res);      } catch (e) {        return reject(e);      }      next(ret);      return null;    }    function onRejected(err) {      var ret;      try {        ret = gen.throw(err);      } catch (e) {        return reject(e);      }      next(ret);    }    function next(ret) {      if (ret.done) return resolve(ret.value);      var value = toPromise.call(ctx, ret.value);      if (value && isPromise(value)) return value.then(onFulfilled, onRejected);      return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '        + 'but the following object was passed: "' + String(ret.value) + '"'));    }  });}
  1. 从整体构造看,co的参数是一个Generator,返回值是一个Promise,简直所有逻辑代码都在这个Promise外面,这也是咱们应用时用then拿后果的起因。
  2. Promise外面先把Generator拿进去执行,失去一个迭代器gen
  3. 手动调用一次onFulfilled,开启迭代

    1. onFulfilled接管一个参数res,第一次调用是没有传这个参数,这个参数次要是用来接管前面的then返回的后果。
    2. 而后调用gen.next,留神这个的返回值ret的模式是{value, done},而后将这个ret传给部分的next
  4. 而后执行部分next,他接管的参数是yield返回值{value, done}

    1. 这里先检测迭代是否实现,如果实现了,就间接将整个promise resolve。
    2. 这里的value是yield前面表达式的值,可能是thunk,也可能是promise
    3. 将value转换成promise
    4. 将转换后的promise拿进去执行,胜利的回调是后面的onFulfilled
  5. 咱们再来看下onFulfilled,这是第二次执行onFulfilled了。这次执行的时候传入的参数res是上次异步promise的执行后果,对应咱们的fetch就是拿回来的数据,这个数据传给第二个gen.next,成果就是咱们代码外面的赋值给了第一个yield后面的变量r1。而后持续部分next,这个next其实就是执行第二个异步Promise了。这个promise的胜利回调又持续调用gen.next,这样就一直的执行上来,直到done变成true为止。
  6. 最初看一眼onRejected办法,这个办法其实作为了异步promise的谬误分支,这个函数外面间接调用了gen.throw,这样咱们在Generator外面能够间接用try...catch...拿到谬误。须要留神的是gen.throw前面还持续调用了next(ret),这是因为在Generator的catch分支外面还可能持续有yield,比方谬误上报的网络申请,这时候的迭代器并不一定完结了。

async/await

最初提一下async/await,先来看一下用法:

const fetch = require('node-fetch');async function sendRequest () {  const r1 = await fetch('https://www.baidu.com');  const r2 = await fetch('https://www.baidu.com');  const r3 = await fetch('https://www.baidu.com');    return {    r1,    r2,    r3,  }}// 留神async返回的也是一个promisesendRequest().then((res) => {  console.log('res', res);});

咋一看这个跟后面promise版的co是不是很像,返回值都是一个promise,只是Generator换成了一个async函数,函数外面的yield换成了await,而且外层不须要co来包裹也能够主动执行了。其实async函数就是Generator加主动执行器的语法糖,能够了解为从语言层面反对了Generator的主动执行。下面这段代码跟co版的promise其实就是等价的。

总结

  1. Generator是一种更古代的异步解决方案,在JS语言层面反对了协程
  2. Generator的返回值是一个迭代器
  3. 这个迭代器须要手动调next能力一条一条执行yield
  4. next的返回值是{value, done},value是yield前面表达式的值
  5. yield语句自身并没有返回值,下次调next的参数会作为上一个yield语句的返回值
  6. Generator本人不能主动执行,要主动执行须要引入其余计划,后面讲thunk的时候提供了一种计划,co模块也是一个很受欢迎的主动执行计划
  7. 这两个计划的思路有点相似,都是先写一个部分的办法,这个办法会去调用gen.next,同时这个办法自身又会传到回调函数或者promise的胜利分支外面,异步完结后又持续调用这个部分办法,这个部分办法又调用gen.next,这样始终迭代,直到迭代器执行结束。
  8. async/await其实是Generator和主动执行器的语法糖,写法和实现原理都相似co模块的promise模式。

文章的最初,感激你破费贵重的工夫浏览本文,如果本文给了你一点点帮忙或者启发,请不要悭吝你的赞和GitHub小星星,你的反对是作者继续创作的能源。

作者博文GitHub我的项目地址: https://github.com/dennis-jiang/Front-End-Knowledges