[TOC]

Generator是什么

  1. ES6提供的异步编程解决方案之一。
  2. Generator函数是一个状态机,内部封装了不同状态的数据。
  3. 用来生成遍历器对象(iterator接口),调用Generator函数并不会执行其内部的代码,而是返回一个遍历器对象
  4. 可以暂停函数(惰性求值),yield实现暂停,next方法可继续执行,每次返回的都是yield表达式后面的结果。

Generator函数特点

  1. function关键字与函数名之间有一个*星号。
  2. 调用Generator函数返回的是一个遍历器对象,而不会执行函数内部逻辑。
  3. 通过next方法分步执行generator函数内部代码,可以实现手动控制执行阶段。
  4. yield表示暂停执行,return表示结束执行。
  5. next方法中可以传递参数,可以从外部向内部传值。
  6. for...of语句可以自动遍历迭代器对象,不需要显式调用next方法。
  7. 内部用yield表达式来定义不同的状态。
  8. 调用next方法,函数内部逻辑开始执行,遇到yield表达式停止向下执行,返回{value: yield后的表达式结果/undefined, done: false/true}。如果Generator函数有return语句,最后返回{value: return的结果(默认是undefined), done: true}
  9. 再次调用next方法会从上一次停止时的yield表达式处开始,直到下一个yield表达式停止或者最后。
  10. yield语句返回结果通常是undefined,当调用next方法时传参内容会作为启动时yield语句的返回值(这里需要注意:下一个next方法的参数值是作为上一个yield表达式的结果)

来看个🌰:

function* generatorExample() {
    console.log('函数开始执行');
    let res = yield 'hello'; // yield语句返回结果默认是为undefined
    console.log(res); // '我是next传入的值'
    console.log('函数继续执行');
    yield 'world';
    // 函数没有return语句的话,默认返回undefined
    return '执行结束';
}

const gen = generatorExample();
console.log(gen.next());
// 当调用next方法时传参内容会作为启动时yield语句的返回值
console.log(gen.next('我是next传入的值'));
console.log(gen.next());
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

结果如下:

函数开始执行
{value: "hello", done: false}
我是next传入的值
函数继续执行
{value: "world", done: false}
{value: "执行结束", done: true} // 函数没有return语句的话将返回{value: undefined, done: true}
1
2
3
4
5
6

Generator函数基本语法

Generator函数语法与传统函数完全不同。

// 该函数有三个状态:两个yield表达式(hello,world)和return语句
function* testGenerator() {
    yield 'hello';
    yield 'world';
    return 'end';
}
const gen = testGenerator();
1
2
3
4
5
6
7

上面代码中定义了一个Generator函数testGenerator,它内部有两个yield表达式,即该函数有三个状态:hello,world和return语句(结束执行)。 形式上,Generator函数是一个普通函数,但是有两个特征:

  1. function关键字与函数名之间有一个*星号;
  2. 函数体内部使用yield(翻译为产出)表达式,定义不同的内部状态;

Generator函数的调用方法与普通函数一样,也是在函数名后面加上一对圆括号。区别在于:调用Generator函数后,该函数并不执行,返回的也不是函数执行结果,而是一个指向内部状态的指针对象,即遍历器对象(Iterator Object)

必须调用遍历器对象的next方法,使得指针移向下一个状态。也就是说,每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield表达式(或return语句)为止。也就是说,Generator函数是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行。

gen.next();
// value值是yield后的表达式结果
// { value: 'hello', done: false }
gen.next();
// { value: 'world', done: false }
gen.next();
// { value: 'end', done: true }
gen.next();
// { value: undefined, done: true }
1
2
3
4
5
6
7
8
9

上面代码一共调用了四次next方法。

  • 第一次调用:Generator函数开始执行,直到遇到第一个yield表达式为止。next方法返回一个对象,它的value属性是当前yield表达式的值hello,done属性的值false,表示遍历还没有结束。
  • 第二次调用:Generator函数从上次yield表达式停下的地方,一直执行到下一个yield表达式。next方法返回的对象的value属性就是当前yield表达式的值world,done属性的值false,表示遍历还没有结束。
  • 第三次调用:Generator函数从上次yield表达式停下的地方,一直执行到return语句(如果没有return语句,就执行到函数结束,其实每个函数都有一个隐式的return语句,默认返回undefined)。next方法返回的对象的value属性,就是紧跟在return语句后面的表达式的值(如果没有return语句,则value属性的值为undefined),done属性的值true,表示遍历已经结束。
  • 第四次调用:此时Generator函数已经运行完毕,next方法返回对象的value属性为undefined,done属性为true。以后再调用next方法,返回的都是这个值。

总结:调用Generator函数,返回一个遍历器对象,代表Generator函数的内部指针。以后,每次调用遍历器对象的next方法,就会返回一个拥有value和done两个属性的对象。value属性表示当前的内部状态的值,该值是yield后面对应表达式的值;done属性是一个布尔值,表示是否遍历结束。

Generator函数实例应用

  1. 发送ajax请求获取新闻内容。
  2. 新闻内容获取成功后再次发送请求,获取对应的新闻评论。
  3. 新闻内容获取失败则不需要再次发送请求。
function* sendRequest() {
    const url = yield getData('http://www.example.com/news?newsID=123');
    yield getData(url);
}

function getData(url) {
    $.get(url, data => {
        console.log(data);
        const commentsUrl = data.commentsUrl;
        const resUrl = `http://www.example.com/${commentsUrl}`;
        // 当获取新闻内容成功,发送请求获取对应的评论内容
        // 调用next传参会作为上次暂停是yield的返回值,即这里的resUrl会传递给url变量
        gen.next(resUrl);
    });
}

const gen = sendRequest();
gen.next(); // 发送获取新闻内容请求
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

给对象添加iterator接口

const objIterable = {name: 'lisi', age: 12};

// TypeError: objIterable is not iterable
for (const i of objIterable) {
    console.log(i);
}
1
2
3
4
5
6

执行上述代码会报错。

const objIterable = {};
objIterable[Symbol.iterator] = function* () {
    yield 'hello';
    yield 'world';
    yield 'end';
}

// for...of语句可以自动遍历迭代器对象,不需要显式调用next方法
for (const i of objIterable) {
    console.log(i);
}

// 扩展运算符也是调用的对象的iterator方法
const obj = [...objIterable];
console.log(obj); // [ 'hello', 'world', 'end' ]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

输出结果如下:

hello
world
end
[ 'hello', 'world', 'end' ]
1
2
3
4

Generator函数的异步应用

Thunk函数

Thunk函数是自动执行Generator函数的一种方法。

参数的求值策略

  • 传值调用
  • 传名调用
const a = 1;
function fn(b) {
    return b * 3;
}

fn(a + 5);
1
2
3
4
5
6

上述代码采用传值调用策略的话,在进入函数体之前,就需要计算a + 5的值(等于6),再将这个值作为实参传入函数fn。C语言就采用这种策略。

fn(a + 5);
// 传值调用相当于
fn(6);
1
2
3

另一种策略是传名调用,即直接将表达式x + 5传入函数体,只在用到它的时候求值。Haskell语言采用这种策略。

fn(a + 5)
// 传名调用时,等同于
(a + 5) * 2
1
2
3

传值调用和传名调用,哪一种比较好?

回答是各有利弊。传值调用比较简单,但是对参数求值的时候,实际上还没用到这个参数,有可能造成性能损失。举个🌰:

function f(a, b) {
  return b;
}

f(3 * x * x - 2 * x - 1, x);
1
2
3
4
5

上面代码中,函数f的第一个参数是一个复杂的表达式,但是函数体内根本没用到。对这个参数求值,实际上是不必要的。因此,有一些计算机学家倾向于传名调用,即只在执行时求值。

Thunk函数的含义

编译器的传名调用实现,往往是将参数放到一个临时函数之中,再将这个临时函数传入函数体。这个临时函数就叫做Thunk函数。

const a = 1;

function thunk() {
    return a + 5;
}
function fn(thunk) {
    return thunk() * 3;
}

const res = fn(thunk);
console.log(res); // 18
1
2
3
4
5
6
7
8
9
10
11

上面代码中,函数fn的参数a + 5被一个函数替换了。凡是用到原参数的地方,对Thunk函数求值即可。这就是Thunk函数的定义,它是传名调用的一种实现策略,用来替换某个表达式。

js中的Thunk函数

js是传值调用,它的Thunk函数含义有所不同。在js中,Thunk函数替换的不是表达式,而是多参数函数,将其替换成一个只接受回调函数作为参数的单参数函数。

const fs = require('fs');

// Thunk函数转换器
function thunk(fn) {
    return function() {
        const args = Array.prototype.slice.call(arguments);
        return function(callback) {
            args.push(callback);
            return fn.apply(null, args);
        }
    }
}

// fs.readFile('./name.txt', 'utf8', (err, data) => {
//     console.log(data.toString());
// });

// 实现Thunk版本的readFile
const thunkReadfile = thunk(fs.readFile);
thunkReadfile('./name.txt', 'utf8')((err, data) => {
    console.log(data.toString());
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// ES6之call版本Thunk函数转换器
const thunk = fn => (...args) => {
    return callback => fn.call(null, ...args, callback);
}
// ES6之apply版本Thunk函数转换器
const thunk = fn => (...args) => {
    return callback => fn.apply(null, args.concat(callback));
}
1
2
3
4
5
6
7
8

任何函数,只要参数有回调函数,就能写成Thunk函数的形式。

Thunkify模块

生产环境的转换器,建议使用Thunkify模块。

yarn add thunkify
1

使用方式如下:

const thunkify = require('thunkify');
const fs = require('fs');

const read = thunkify(fs.readFile);
read('package.json')((err, data) => {
  // ...
});
1
2
3
4
5
6
7

Generator函数的流程管理

function* gen() {
    yield 1;
    yield 2;
    yield 3;
}

const g = gen();
let res = g.next();

// 自动执行
while(!res.done) {
    console.log(res);
    res = g.next();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

co模块

co模块的主要作用是:用于Generator函数的自动执行。

co具体做的事情:

  1. 接受一个generator函数作为输入,返回一个Promise对象;
  2. 遍历整个generator(即不断的调用next);
    1. 在遍历结束时(即next返回的对象done: false)进行resolve,resolve所持有的值是最后一个next输出的value
    2. 在遍历过程中出现错误则 reject
  3. 仅支持generator函数中yield非空对象(不支持 primitive types 如 number, string 等)。

co实际对外提供了2种api:

  • 无参数的:co(fn *)
  • 有参数的:co.wrap(fn *)

来看个无参数的🌰:

const {fs} = require('mz');
const co = require('co');
function* gen() {
    // 依次读取两个文件
    const name = yield fs.readFile('name.txt', 'utf8');
    const age = yield fs.readFile('age.txt', 'utf8');
    console.log(name.toString());
    console.log(age.toString());
}

co(gen);
1
2
3
4
5
6
7
8
9
10
11

上面代码中,Generator函数只要传入co函数,就会自动执行。输出结果如下:

lisi
18
1
2

co函数返回一个Promise对象,因此可以用then方法添加回调函数。

co(gen).then(() => {
    console.log('gen函数执行完毕');
});
1
2
3

上面代码中,等到Generator函数执行结束,就会输出一行提示。

const {fs} = require('mz');
const co = require('co');

function* read() {
    const name = yield fs.readFile('name.txt', 'utf8');
    const age = yield fs.readFile('age.txt', 'utf8');
    const a = yield [1, 2, 3];
    return age + a;
}
// co接收generator参数,返回的是Promise
co(read).then(data => console.log(data)); // 121,2,3
1
2
3
4
5
6
7
8
9
10
11
// mz模块把node中的一些常用模块promise化了
const {fs} = require('mz');
function* read() {
    let filename = yield fs.readFile('./name.txt', 'utf8');
    let age = yield fs.readFile(filename, 'utf8');
    let b = yield [1, 2, 3];
    return age + b;
}
// const co = require('co');
// 实现co
function co(it) {// express koa
    // 返回的是Promise
    return new Promise((resolve, reject) => {
        function next(r) { // 如果碰到异步迭代 需要借助一个自执行函数来实现,保证第一次执行后调用下一次执行
            const {value, done} = it.next(r);
            if(!done) { // babel
                // 不管value是什么类型,都包装成promise
                Promise.resolve(value).then(r => {
                    next(r);
                }, err => {
                    reject(err);
                });
            } else { // 完成了走成功
                resolve(value);
            }
        }
        next();
    });
}
// co接收generator参数,返回的是Promise
co(read()).then(data=>{
    console.log(data);
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

再来看个有参数的🌰:

const co = require('co');

function* gen() {
    console.log(arguments); // { '0': 123, '1': 456, '2': 789 }
    const a = Promise.resolve(1);
    const b = Promise.resolve(2);
    const  c= Promise.resolve(3);
    const res = yield [a, b, c];
    return res;
}

const coWrap = co.wrap(gen);
// [ 1, 2, 3 ]
coWrap(123, 456, 789).then(
    data => {
        console.log(data);
    }).catch(err => console.log(err));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

co核心原理

co的核心原理在于:next函数,这里将每一次yield的返回值包装成Promise对象,在Promise的onFulfilled和onRejected状态中继续递归调用next函数,保证链式调用自动执行,使得异步的代码能够以同步的方式运行。

将yield返回对象的value值转换为一个Promise对象,执行该Promise即可拿到程序的执行权。然后通过在onFulfilled和onRejected中继续调用next方法可以交还程序执行权,如此达到自动执行generator函数的效果。

Generator函数实现斐波那契数列

// 递归实现
function fibonacci(n) {
    if (n < 2) return n;
    return fibonacci(n - 1) + fibonacci(n - 2);
}
1
2
3
4
5
function* gen() {
    let [pre, cur] = [0, 1];
    for(;;) {
        [pre, cur] = [cur, pre + cur];
        yield cur;
    }
}

for (let i of gen()) {
    if (i > 1000) break;
    console.log(i);
}
1
2
3
4
5
6
7
8
9
10
11
12

参考文档

  1. Generator 函数的语法
  2. 深入理解koa中的co源码
  3. 你不懂JS: 异步与性能-Generator
  4. 基于 Generator 与 Promise 的异步编程解决方案