Generator 101

function * HelloGen(){
  yield 100;
  yield 400;
}

var gen = HelloGen();

console.log(gen.next()); // {value: 100, done: false}
console.log(gen.next()); // {value: 400, done: false}
console.log(gen.next()); // {value: undefined, done: true}
`</pre>

<pre>`function * HelloGen2() {
  var a = yield 100;
  var b = yield a + 100;

  console.log(b);
}

var gen2 = HelloGen2();

console.log(gen2.next()); // {value: 100, done: false}
console.log(gen2.nect(500)); // {value: 600, done: false}
console.log(gen2.next(1000)); // {value: undefined, done: done}
// prints 1000
`</pre>

### Preventing Callback Hell

So, how can generators be used to avoid callback hell? First, you need to understand a simple technique that we will be using heavily with generators to write code without callbacks.

#### Understanding Thunks

A thunk is a partially evaluated function which accepts a single callback as the argument. Within generators, we'll be yielding thunks to write without callbacks. A simple thunk is shown below.

<pre>`function(callback) {
  fs.readFile('myfile.md', 'utf8', callback)
}
`</pre>

Thunks can also be created dynamically as shown below:

<pre>`function readFile(filename) {
  return function(callback) {
    fs.readFile(filename, 'utf8', callback);
  };
}
`</pre>

#### Using `co`

`co` is a nice module which helps to use thunks and generators together to create Node.js applications without callbacks.

A simple application which uses `co` and the `readFile()` thunk.

<pre>`var co = require('co');

co(function* (){
  var file1 = yield readFile('file1.md');
  var file2 = yield readFile('file2.md');

  console.log(file1);
  console.log(file2);
})();
`</pre>

As you can see, we are no longer using callbacks. This gives us a simple way to write large modular Node apps easily.

###### How `co` Works Internally
  1. First, it calls next(null) and gets a thunk.
  2. Then, it evaluate the thunk and saves the result.
  3. Then, it calls next(savedResult)(in this case, saveResult == readFile(‘file1.md’), and next(savedResult) pass the content to var file1)
  4. Repeat these steps until next() return {done: true}

    A minimal version of co written to show how it works internally.

    `
    function co(generator){
    var gen = generator();

    function nextItem(err, result) {

    var item = gen.next(result);
    

    `if (!item.done) {
    item.value(nextItem); // item.value is a thunk, because yield a thunk
    }

}

nextItem();
}

彻底理解 Thunk 函数

生成器函数

生成器(Generator)函数写成:

function* func () {}
`</pre>

其本质也是一个函数, 所以他具备普通函数所具有的所有特性, 除此以外, 他还具有以下特性
  1. 执行生成器函数后返回一个迭代器(Iterator)
  2. 生成器函数内部可以使用 yield 或 yield*, 函数执行到 yield 的时候会暂停执行, 并返回 yield 后面表达式的值, 通过迭代器的 next() 方法返回一个对象, 该对象由 value 和 done 属性组成, 其中, value 属性值为 yield 后面表达式的值, done 属性表示生成器的执行状态
  3. 迭代器 next() 方法的参数是yield 表达式的值
  4. 当 next 指向最后一个 yield 的时候, 再次执行 next() 方法, 返回的对象中 done: true, 而 value 的值为 return 后面的表达式(默认为 undefined)

    例一

    `function test() {
      return 'b';
    }
    function* func() {
      var a = yield 'a';
    
      console.log('gen', a); // 'gen: undefined' - (如果 next 方法不带参数, yield 表达式返回 undefined)
      var b = yield test();
      console.log('gen': b); // 'gen: undefined'
    }
    
    var func1 = func();
    var a = func1.next();
    console.log('next: ' a); // 'next: {value: 'a', done: false}'
    var b = func1.next();
    console.log('next: ' b); // 'next: {value: 'b', done: false}'
    var c = func1.next();
    console.log('next: ' c); // 'next: {value: undefined, done: true}'
    `

    关于 yield*

    yield 暂停执行并返回后面表达式的值, 而 yield* 则将函数委托到另一个迭代器或可迭代对象

    arguments

    `> function* genFunc(){
    ... yield arguments;
    ... yield* arguments;
    ... }
    undefined
    > var gen = genFunc(1,2);
    undefined
    > gen.next()
    { value: { '0': 1, '1': 2 }, done: false }
    > gen.next()
    { value: 1, done: false }
    > gen.next()
    { value: 2, done: false }
    > gen.next()
    { value: undefined, done: true }
    `

    Generator

    `> function * gen1(){
    ... yield 2;
    ... yield 3;
    ... }
    undefined
    > function * gen2(){
    ... yield 1;
    ... yield* gen1();
    ... yield 4;
    ... }
    undefined
    > var g2 = gen2();
    undefined
    > g2.next()
    { value: 1, done: false }
    > g2.next()
    { value: 2, done: false }
    > g2.next()
    { value: 3, done: false }
    > g2.next()
    { value: 4, done: false }
    > g2.next()
    { value: undefined, done: true }
    `

    Thunk 函数

    在 co 的应用中, 为了能像同步代码那样书写异步代码, 比较多的使用方式是使用 thunk 函数(但不是唯一方式, 还可以是 Promise), 比如读取文件内容的异步函数 fs.readFile() 方法, 转化为 thunk 函数的方式如下

    `function readFile(path, encoding){
      return function(cb){
        fs.readFile(path, encoding, cb);
      }
    }
    `

    Thunk函数具备以下两个要素
    1. 有且只有一个参数是 callback 的函数
    2. callback 的第一个参数是 error

    使用 thunk 函数, 同时结合 co 我们就可以像书写同步代码一样书写异步代码

    `var co = require('co');
    var fs = require('fs');
    var Promise = require('es6-promise').Promise;
    
    function readFile(path, encoding){
      return function(cb) { // thunk function
        fs.readFile(path, encoding, cd);
      };
    }
    
    co(function* () { // 外部不可见, 但在 co 内部其实已经转化成promise.then().catch()...链式调用的行驶
      var a = yield readFile('a.txt', {encoding: 'utf-8'});
      console.log(a); // a
      var b = yield readFile('b.txt', {encoding: 'utf-8'});
      console.log(b); // b
      return yield Promise.resolve(a+b);
    }).then(function(val){
      console.log(val) // ab
    }).catch(function(error){
      console.log(error);
    })
    `

    有一个框架 thunkify 可以帮我们轻松实现以上步骤

    `var co = require('co');
    var thunkify = require('thunkify');
    var fs = requrie('fs');
    var Promise = require('es6-promise').Promise;
    
    var readFile = thunkify(fs.readFile);
    
    co(function* () { // 外部不可见, 但在 co 内部已经转化为 promise 的链式调用
      var a = yield readFile('a.txt', {encoding: 'utf-8'});
      console.log(a); // a
      var b = yield readFile('b.txt', {encoding: 'utf-8'});
      console.log(b); // b
      return yield Promise.resolve(a+b);
    }).then(function(val){
      console.log(val); // ab
    }).catch(function(err){
      console.log(err);
    })
    `

    对于 thunkify 框架的理解注释如下:

    `/**

    • Module dependencies
      */
      var assert = require(‘assert’);

    /**

    • Expose thunkify().
      */

    module.exports = thunkify;

    function thunkify(fn) {
    assert(‘function’ == typeof fn, ‘function required’);

    // 返回一个包含 thunk 函数的函数, 返回的 thunk 函数用于执行yield, 而外层的这个函数用于给 thunk 函数传递其他参数
    return function() {

    var args = new Array(arguments.length);
    // 缓存上下文环境, 给 fn 提供执行环境
    var ctx = this;
    
    // 将参数类数组对象转为数组(实现方式略臃肿, 可以直接用Array.from)
    for (var i = 0; i &lt; args.length; ++i){
      args[i] = argumetns[i];
    }
    
    // 真正的 thunk 函数(有且只有一个参数, 并且参数是第一个参数为 err 的 callback)
    return function(done) { // 返回 thunk 函数
      var called;
    
      // 将回调函数再包裹一层, 避免重复调用, 同时, 将包裹了的真正的回调函数 push 进参数数组(即将回调函数 done 作为最后一个参数传递给 fn)
      args.push(function(){  // 将回调函数 done 加到参数列表尾部, 形成 fn 需要的 [arg1, ..., done]
        if (called) return;
        called = true;
        done.apply(null, arguments); // 执行回调函数
      });
    
      try {
        // 在 ctx 环境执行 fn
        // 并将执行 thunkify 之后返回的函数的参数(含 done 回调) 传入, 类似于执行:
        // fs.readFile(path, {encoding: 'utf-8'}, done)
        fn.apply(ctx,args); // 类似 fn(arg1, .., done)
      } catch(err) {
        done(err);
      }
    }
    

    }
    }

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×