乐趣区

关于javascript:从Generator入手读懂co模块源码

这篇文章是讲 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();

// 手动调第一个 next
itor.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 转换成 thunk
const 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 返回的也是一个 promise
sendRequest().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

退出移动版