这里面其实有两个问题:

  1. Generator 如何跟异步产生关系?
  2. 怎么把 Generator 按顺序执行完毕?

thunk 函数

要想知道 Generator 跟异步的关系,首先带大家搞清楚一个概念——thunk函数(即偏函数),虽然这只是实现两者关系的方式之一。(另一种方式是Promise, 后面会讲到)

举个例子,比如我们现在要判断数据类型。可以写如下的判断逻辑:

  1. let isString = (obj) => {
  2. return Object.prototype.toString.call(obj) === '[object String]';
  3. };
  4. let isFunction = (obj) => {
  5. return Object.prototype.toString.call(obj) === '[object Function]';
  6. };
  7. let isArray = (obj) => {
  8. return Object.prototype.toString.call(obj) === '[object Array]';
  9. };
  10. let isSet = (obj) => {
  11. return Object.prototype.toString.call(obj) === '[object Set]';
  12. };
  13. // ...

可以看到,出现了非常多重复的逻辑。我们将它们做一下封装:

  1. let isType = (type) => {
  2. return (obj) => {
  3. return Object.prototype.toString.call(obj) === `[object ${type}]`;
  4. }
  5. }

现在我们这样做即可:

  1. let isString = isType('String');
  2. let isFunction = isType('Function');
  3. //...

相应的 isStringisFunction是由isType生产出来的函数,但它们依然可以判断出参数是否为String(Function),而且代码简洁了不少。

  1. isString("123");
  2. isFunction(val => val);

isType这样的函数我们称为thunk 函数。它的核心逻辑是接收一定的参数,生产出定制化的函数,然后使用定制化的函数去完成功能。thunk函数的实现会比单个的判断函数复杂一点点,但就是这一点点的复杂,大大方便了后续的操作。

Generator 和 异步

thunk 版本

文件操作为例,我们来看看 异步操作 如何应用于Generator

  1. const readFileThunk = (filename) => {
  2. return (callback) => {
  3. fs.readFile(filename, callback);
  4. }
  5. }

readFileThunk就是一个thunk函数。异步操作核心的一环就是绑定回调函数,而thunk函数可以帮我们做到。首先传入文件名,然后生成一个针对某个文件的定制化函数。这个函数中传入回调,这个回调就会成为异步操作的回调。这样就让 Generator异步关联起来了。

紧接者我们做如下的操作:

  1. const gen = function* () {
  2. const data1 = yield readFileThunk('001.txt')
  3. console.log(data1.toString())
  4. const data2 = yield readFileThunk('002.txt')
  5. console.log(data2.toString)
  6. }

接着我们让它执行完:

  1. let g = gen();
  2. // 第一步: 由于进场是暂停的,我们调用next,让它开始执行。
  3. // next返回值中有一个value值,这个value是yield后面的结果,放在这里也就是是thunk函数生成的定制化函数,里面需要传一个回调函数作为参数
  4. g.next().value((err, data1) => {
  5. // 第二步: 拿到上一次得到的结果,调用next, 将结果作为参数传入,程序继续执行。
  6. // 同理,value传入回调
  7. g.next(data1).value((err, data2) => {
  8. g.next(data2);
  9. })
  10. })

打印结果如下:

  1. 001.txt的内容
  2. 002.txt的内容

上面嵌套的情况还算简单,如果任务多起来,就会产生很多层的嵌套,可操作性不强,有必要把执行的代码封装一下:

  1. function run(gen){
  2. const next = (err, data) => {
  3. let res = gen.next(data);
  4. if(res.done) return;
  5. res.value(next);
  6. }
  7. next();
  8. }
  9. run(g);

Ok,再次执行,依然打印正确的结果。代码虽然就这么几行,但包含了递归的过程,好好体会一下。

这是通过thunk完成异步操作的情况。

Promise 版本

还是拿上面的例子,用Promise来实现就轻松一些:

  1. const readFilePromise = (filename) => {
  2. return new Promise((resolve, reject) => {
  3. fs.readFile(filename, (err, data) => {
  4. if(err) {
  5. reject(err);
  6. }else {
  7. resolve(data);
  8. }
  9. })
  10. }).then(res => res);
  11. }
  12. const gen = function* () {
  13. const data1 = yield readFilePromise('001.txt')
  14. console.log(data1.toString())
  15. const data2 = yield readFilePromise('002.txt')
  16. console.log(data2.toString)
  17. }

执行的代码如下:

  1. let g = gen();
  2. function getGenPromise(gen, data) {
  3. return gen.next(data).value;
  4. }
  5. getGenPromise(g).then(data1 => {
  6. return getGenPromise(g, data1);
  7. }).then(data2 => {
  8. return getGenPromise(g, data2)
  9. })

打印结果如下:

  1. 001.txt的内容
  2. 002.txt的内容

同样,我们可以对执行Generator的代码加以封装:

  1. function run(g) {
  2. const next = (data) => {
  3. let res = g.next();
  4. if(res.done) return;
  5. res.value.then(data => {
  6. next(data);
  7. })
  8. }
  9. next();
  10. }

同样能输出正确的结果。代码非常精炼,希望能参照刚刚链式调用的例子,仔细体会一下递归调用的过程。

采用 co 库

以上我们针对 thunk 函数Promise两种Generator异步操作的一次性执行完毕做了封装,但实际场景中已经存在成熟的工具包了,如果大名鼎鼎的co库, 其实核心原理就是我们已经手写过了(就是刚刚封装的Promise情况下的执行代码),只不过源码会各种边界情况做了处理。使用起来非常简单:

  1. const co = require('co');
  2. let g = gen();
  3. co(g).then(res =>{
  4. console.log(res);
  5. })

打印结果如下:

  1. 001.txt的内容
  2. 002.txt的内容
  3. 100

简单几行代码就完成了Generator所有的操作,真不愧coGenerator天生一对啊!