[TOC]
Generator是什么
- ES6提供的异步编程解决方案之一。
- Generator函数是一个状态机,内部封装了不同状态的数据。
- 用来生成遍历器对象(iterator接口),调用Generator函数并不会执行其内部的代码,而是返回一个遍历器对象。
- 可以暂停函数(惰性求值),yield实现暂停,next方法可继续执行,每次返回的都是yield表达式后面的结果。
Generator函数特点
- function关键字与函数名之间有一个
*
星号。 - 调用Generator函数返回的是一个遍历器对象,而不会执行函数内部逻辑。
- 通过next方法分步执行generator函数内部代码,可以实现手动控制执行阶段。
- yield表示暂停执行,return表示结束执行。
- next方法中可以传递参数,可以从外部向内部传值。
for...of
语句可以自动遍历迭代器对象,不需要显式调用next方法。- 内部用yield表达式来定义不同的状态。
- 调用next方法,函数内部逻辑开始执行,遇到yield表达式停止向下执行,返回
{value: yield后的表达式结果/undefined, done: false/true}
。如果Generator函数有return语句,最后返回{value: return的结果(默认是undefined), done: true}
。 - 再次调用next方法会从上一次停止时的yield表达式处开始,直到下一个yield表达式停止或者最后。
- 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());
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}
2
3
4
5
6
Generator函数基本语法
Generator函数语法与传统函数完全不同。
// 该函数有三个状态:两个yield表达式(hello,world)和return语句
function* testGenerator() {
yield 'hello';
yield 'world';
return 'end';
}
const gen = testGenerator();
2
3
4
5
6
7
上面代码中定义了一个Generator
函数testGenerator,它内部有两个yield表达式,即该函数有三个状态:hello,world和return语句(结束执行)。
形式上,Generator
函数是一个普通函数,但是有两个特征:
- function关键字与函数名之间有一个
*
星号; - 函数体内部使用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 }
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函数实例应用
- 发送ajax请求获取新闻内容。
- 新闻内容获取成功后再次发送请求,获取对应的新闻评论。
- 新闻内容获取失败则不需要再次发送请求。
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(); // 发送获取新闻内容请求
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);
}
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' ]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
输出结果如下:
hello
world
end
[ 'hello', 'world', 'end' ]
2
3
4
Generator函数的异步应用
Thunk函数
Thunk函数是自动执行Generator函数的一种方法。
参数的求值策略
- 传值调用
- 传名调用
const a = 1;
function fn(b) {
return b * 3;
}
fn(a + 5);
2
3
4
5
6
上述代码采用传值调用策略的话,在进入函数体之前,就需要计算a + 5的值(等于6),再将这个值作为实参传入函数fn。C语言就采用这种策略。
fn(a + 5);
// 传值调用相当于
fn(6);
2
3
另一种策略是传名调用,即直接将表达式x + 5
传入函数体,只在用到它的时候求值。Haskell语言采用这种策略。
fn(a + 5)
// 传名调用时,等同于
(a + 5) * 2
2
3
传值调用和传名调用,哪一种比较好?
回答是各有利弊。传值调用比较简单,但是对参数求值的时候,实际上还没用到这个参数,有可能造成性能损失。举个🌰:
function f(a, b) {
return b;
}
f(3 * x * x - 2 * x - 1, x);
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
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());
});
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));
}
2
3
4
5
6
7
8
任何函数,只要参数有回调函数,就能写成Thunk函数的形式。
Thunkify模块
生产环境的转换器,建议使用Thunkify模块。
yarn add thunkify
使用方式如下:
const thunkify = require('thunkify');
const fs = require('fs');
const read = thunkify(fs.readFile);
read('package.json')((err, data) => {
// ...
});
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();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
co模块
co模块的主要作用是:用于Generator函数的自动执行。
co具体做的事情:
- 接受一个generator函数作为输入,返回一个Promise对象;
- 遍历整个generator(即不断的调用next);
- 在遍历结束时(即next返回的对象done: false)进行resolve,resolve所持有的值是最后一个next输出的value
- 在遍历过程中出现错误则 reject
- 仅支持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);
2
3
4
5
6
7
8
9
10
11
上面代码中,Generator函数只要传入co函数,就会自动执行。输出结果如下:
lisi
18
2
co函数返回一个Promise对象,因此可以用then方法添加回调函数。
co(gen).then(() => {
console.log('gen函数执行完毕');
});
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
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);
});
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));
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);
}
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);
}
2
3
4
5
6
7
8
9
10
11
12