参考原文:http://es6.ruanyifeng.com/#docs/generator
Generator
函数有多种理解角度。语法上,首先可以把它理解成,Generator
函数是一个状态机,封装了多个内部状态。
执行 Generator
函数会返回一个遍历器对象,也就是说,Generator
函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator
函数内部的每一个状态。
形式上,Generator
函数是一个普通函数,但是有两个特征。一是,function
关键字与函数名之间有一个星号;二是,函数体内部使用 yield
表达式。
实例
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();
console.log(hw.next());
console.log(hw.next());
console.log(hw.next());
console.log(hw.next());
// 输出结果:
// { value: 'hello', done: false }
// { value: 'world', done: false }
// { value: 'ending', done: true }
// { value: undefined, done: true }
上面代码定义了一个 Generator
函数 helloWorldGenerator
,调用 Generator
函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,也就是遍历器对象(Iterator Object)。
要使 Generator
函数执行,可以调用遍历器对象的 next
方法,使得指针移向下一个状态。也就是说,每次调用 next
方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个 yield
表达式(或 return
语句)为止。
当执行到 return
语句时候,done
属性变为 true
,表示遍历结束,后面的代码不再执行。
当 Generator
函数已经运行完毕,next
方法返回对象的 value
属性为 undefined
,done
属性为 true
。以后再调用 next
方法,返回的都是这个值。
换言之,Generator
函数是分段执行的,yield
表达式是暂停执行的标记,而 next
方法可以恢复执行。
再看一个循环的例子:
function* f(x) {
for (i=0; i<x; i++) {
yield i;
};
};
var hw = f(3);
console.log(hw.next());
console.log(hw.next());
console.log(hw.next());
console.log(hw.next());
// 输出结果:
// { value: 0, done: false }
// { value: 1, done: false }
// { value: 2, done: false }
// { value: undefined, done: true }
next 的参数
yield
表达式本身没有返回值,或者说总是返回 undefined
。
function* f() {
const result = yield '123';
console.log(result);
}
var g = f();
g.next()
g.next()
// 输出结果
// undefined
上面代码第一个 next
执行到 yield '123'
结束,第二个 next
执行下面的 console.log(result)
输出第一次 yield
的返回值 undefined
。
next
方法可以带一个参数,该参数就会被当作上一个 yield
表达式的返回值。
function* f() {
for(var i=0; true; i++) {
let reset = yield i;
if (reset) {i = -10}
}
}
var g = f();
console.log(g.next())
console.log(g.next())
console.log(g.next(true))
console.log(g.next())
// 输出结果
// { value: 0, done: false }
// { value: 1, done: false }
// { value: -9, done: false }
// { value: -8, done: false }
上面代码先定义了一个可以无限运行的 Generator
函数,如果 next
方法没有参数,每次运行到 yield
表达式,变量 reset
的值总是 undefined
。
当 next
方法带一个参数 true
时,变量 reset
就被重置为这个参数(即 true
),因此 i
会等于 -10,下一轮循环就会从 -10 开始递增。
再看这个例子会更清楚了解:
function* f() {
for(var i=0; true; i++) {
let reset = yield i;
if (reset) {
console.log(reset);
i = -10
} else {
console.log(reset);
}
}
}
var g = f();
console.log(g.next())
console.log(g.next())
console.log(g.next(true))
console.log(g.next())
// 输出结果:
// { value: 0, done: false }
// undefined
// { value: 1, done: false }
// true
// { value: -9, done: false }
// undefined
// { value: -8, done: false }
这个功能有很重要的语法意义。Generator
函数从暂停状态到恢复运行,它的上下文状态(context
)是不变的。通过 next
方法的参数,就有办法在 Generator
函数开始运行之后,继续向函数体内部注入值。
function* foo(x) {
var y = 2 * (yield (x + 1));
var z = yield (y / 3);
return (x + y + z);
}
var a = foo(5);
console.log(a.next())
console.log(a.next())
console.log(a.next())
// 输出结果
// { value: 6, done: false }
// { value: NaN, done: false }
// { value: NaN, done: true }
上面代码第二次执行 next
的时候 y
的值是 undefined
,所以 y / 3
也是 undefined
;第三次执行 next
同理。
下面我们给 next
带上参数:
var a = foo(5);
console.log(a.next()) // 5+1=6
console.log(a.next(3)) // (2*3)/3=2
console.log(a.next(2)) // 5+(2*3)+2=13
// 输出结果
// { value: 6, done: false }
// { value: 2, done: false }
// { value: 13, done: true }
上面代码第二次调用 next
的时候把 yield
的返回值 yield (x + 1)
设置为 3,因为 y = 2 * (yield (x + 1))
,所以 y
的值就是 6,yield (y / 3)
的值就是 2。
第三次调用 next
的时候把 yield
的返回值 yield (y / 3)
设置为 2,所以 z
等于 2,之前的 y
= 6,所以 x+y+z
的值是 13。
注意,由于 next
方法的参数表示上一个 yield
表达式的返回值,所以在第一次使用 next
方法时,传递参数是无效的。
for...of 循环
for...of
循环可以自动遍历 Generator
函数运行时生成的 Iterator
对象,且此时不再需要调用 next
方法。
function* foo() {
yield 1;
yield 2;
yield 3;
yield 4;
yield 5;
return 6;
}
for (let v of foo()) {
console.log(v);
}
// 1 2 3 4 5
上面代码使用 for...of
循环,依次显示 5 个 yield
表达式的值。这里需要注意,一旦 next
方法的返回对象的 done
属性为 true
,for...of
循环就会中止,且不包含该返回对象,所以上面代码的 return
语句返回的 6,不包括在 for...of
循环之中。
再看一个例子:
function* f(x) {
for (i=0; i<x; i++) {
yield i;
};
};
for (let v of f(10)) {
console.log(v);
};
// 0 1 2 3 ....
Generator 函数的异步应用
下面看看如何使用 Generator
函数,执行一个真实的异步任务。
var fetch = require('node-fetch');
function* gen(){
var url = 'https://api.github.com/users/github';
var result = yield fetch(url);
console.log(result.name);
}
上面代码中,Generator
函数封装了一个异步操作,该操作先读取一个远程接口,然后从 JSON 格式的数据解析信息。就像前面说过的,这段代码非常像同步操作,除了加上了 yield
命令。
执行这段代码的方法如下。
var g = gen();
// 执行异步任务的第一阶段 fetch(url), 返回一个 Promise 对象
// result 的值是 { value: Promise { <pending> }, done: false }
var result = g.next();
// result.value 是 一个 Promise 对象
// 当 fetch(url) 执行完成后运行第一个 then
// 其参数 data 来自 Promise 对象的 resolve 函数的参数
// 并把执行结果 data.json() 传入给第二个 then
// 第二个 then 执行 yield 后面的代码,即: console.log(result.name)
// 第二个 then 的 next 函数带有参数 data,它来自第一个 then 函数的 return
// 所以 result 的值是 data.json()
result.value
.then((data) => {
return data.json();
})
.then((data) => {
g.next(data);
});