基础概念
Generator 函数是一个状态机,封装了多个内部状态。同时也是一个遍历器对象生成函数。返回的遍历器对象,可以一次遍历Generator 函数内部的每个状态。
function* generator() {
yield 'hello'
yield 'world'
return 'ending'
}
var gen = generator()
gen.next()
// { value: 'hello', done: false }
gen.next()
// { value: 'world', done: false }
gen.next()
// { value: 'ending', done: false }
gen.next()
// { value: undefined, done: false }
yield 表达式
由于 Generator 函数返回的遍历器对象,只有调用 next 方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。yield表达式就是暂停的标志。
遍历器对象 next 的执行逻辑
- 遇到 yield 表达式,就暂停执行后面的操作,并将紧跟在 yield 后面的那个表达式的值,作为返回的对象的 value 属性值。
- 下次调用 next 方法时,再继续往下执行,直到遇到下一个 yield 表达式。 3.如果没有再遇到新的 yield 表达式,就一直运行到函数结束,知道 return语句为止,并将return语句后面的表达式的值,作为返回对象的 value的值。
- 如果没有return 语句,则返回的对象 value 属性值为 undefined。
yield 表达式后面的表达式,只有当调用 next 方法、内部指针指向该语句时才会执行。
function *gen () {
yield 123 + 456
}
上面代码 yield 后面表达式不会立即执行,只会在 next 方法将指针移到这一句时,才会求值。
generator 函数可以不用 yield 表达式,这时就变成了一个淡出的暂缓执行函数。
function* f(){
console.log('执行了')
}
var gen = f()
setTimeout(() => {
gen.next()
}, 2000)
上面代码中, f 如果是普通函数,在为变量 gen 赋值的时候就会执行。但是函数 f 是 generator 函数,就只有调用 next 方法时,函数 f 才会执行。
与 Iterator 接口的关系
之前我们说过,任意一个 Symbol.iterator 方法,等于该对象的遍历器生成函数,调用该函数会返回该对象的遍历器对象。
由于 Generator 函数就是遍历器生成函数,因此可以把 Generator 赋值给对象的 Symbol.iterator属性,从而使得该对象具有 Iterator 接口。
var itertor = {}
itertor[Symbol.iterator] = function* () {
yield 1;
yield 2;
yield 3;
}
[...itertor] // 1 2 3
// 原来是这样写的
itertor[Symbol.iterator] = function () {
var current = 0
return {
next: function(){
if(current < 3) {
current++
return {
value: current,
done: false
}
} else {
return {
value: undefined,
done: true
}
}
}
}
}
generator 函数执行后,返回一个遍历器对象。该对象本身也具有 Symbol.iterator 属性,执行后返回自身。
function* gen() {}
var g = gen()
g[Symbol.iterator]() === g
next 方法的参数
yield 表达式本身没有返回值,或总是返回 undefined。next 方法可以带一个参数,该参数就会被当作上一个 yield 表达式的返回值。
function *f() {
for(var i = 0; true; i++) {
var reset = yield i
if(reset) { i = -1; }
}
}
var g = f()
g.next() // {value: 0, done: false}
g.next() // {value: 1, done: false}
g.next(true) // {value: 0, done: false}
next 方法可以带一个参数,这个参数会被当做上一个 yield 的返回值,所以这里传值 true,会被当做上一轮的返回值,赋值给了 reset,然后重置了 reset 的值,在执行 for 循环,然后得到 value 为 0。
如果要在第一次调用 next 方法时,就能输入值,就需要在 Generator 函数外在包一层
function wrapper(generatorFunction) {
return function (...args) {
let generatorObject = generatorFunction(...args);
generatorObject.next();
return generatorObject;
};
}
const wrapped = wrapper(function* () {
console.log(`First input: ${yield}`);
return 'DONE';
});
let g = wrapped()
g.next('hello!')
这里实际上就是在 return generator 对象时之前,先执行了一遍 不传参的 next 方法。
for...of 循环
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
通过 Generator 函数为对象添加 Iterator 接口,可以使用 for...of
function* objectEntries(obj) {
let propKyes = Reflect.ownKeys(obj)
for (let propKey of propKyes) {
yield [propKey, obj[propKey]]
}
}
let jane = { first: 'Jane', last: 'Doe' };
for (let [key, value] of objectEntries(jane)) {
console.log(`${key}: ${value}`);
}
// first: Jane
// last: Doe
Generator.prototype.throw
var g = function* () {
try {
yield
} catch (e) {
console.log('内部捕获', e)
}
}
var i = g()
try {
i.throw('a')
i.throw('b')
} catch(e) {
console.log('外部捕获', e)
}
// 内部捕获 a
// 外部捕获 b
上面代码我们可以看到遍历器 i 连续抛出2个错误。第一个错误被Generator 函数体内部的 catch 语句捕获。 i 第二次抛出错误,由于 Generator 语句一句执行过了,不会再捕捉到这个错误了,所以这个错误就被抛出 Generator 函数体了,被函数体外的 catch 捕获了。
Generator.prototype.return
Generator 函数返回的遍历器对象,还有一个return方法,可以返回给定的值,并且终结遍历 Generator 函数。
function* gen() {
yield 1;
yield 2;
yield 3;
}
var g = gen()
g.next() // { value: 1, done: false }
g.return('foo') // { value: foo, done: true }
g.next() // { value: undefined, done: true }
yield* 表达式
如果再 Generator 函数内部,调用另一个 Generator 函数。需要在前者的函数体内部,自己手动完成遍历。
function* foo() {
yield 'a'
yield 'b'
}
function* bar() {
yield 'x'
// 手动遍历 foo()
for (let i of foo()) {
console.log(i)
}
yield 'y'
}
for (let v of bar()) {
cosole.log(v)
}
// x
// a
// b
// y
上述代码中,foo 和 bar 都是 Generator 函数,在 bar 里面调用 foo,就需要手动遍历 foo,如果多个 Generator 函数嵌套就很麻烦。现在我们使用 yield* 表达式来解决.
function* bar() {
yield 'x'
yield* foo()
yield 'y'
}
// 等同于
function* bar() {
yield 'x'
yield 'a'
yield 'b'
yield 'y'
}
Generator 与状态机
var ticking = true
var clock = function() {
if (ticking)
console.log('Tick!')
else
console.log('Tock!')
ticking = !ticking
}
// 如果用 Generator 的方式
var clock = function* () {
while (true) {
console.log('Tick!')
yield;
console.log('Tock!')
yield;
}
}
应用
异步操作的同步化表达
ajax 是典型的异步操作,通过 generator 函数部署 ajax 操作
function* main(){
var result = yield request('http://test.url')
var resp = JSON.parse(result)
console.log(resp.value)
}
function request(url) {
makeAjax(url, function(response) {
it.next()
})
}
var it = main()
it.next()
控制流管理
如果一个异步操作非常多,采用回调函数,可能会写成下面这样
step1(function(value1) {
step2(value1, function(value2) {
step3(value2, function(value3) {
...
})
})
})
// 使用 Promise 的方式
Promise.resolve(step1)
.then(step2)
.then(step3)
.then(function (value3) {
...
}, function(error) {
...
})
.done()
// 使用 Generator
function* longRunningTask(value1) {
try {
var value2 = yield step1(value1);
var value3 = yield step2(value2);
var value4 = yield step3(value3);
var value5 = yield step4(value4);
// Do something with value4
} catch (e) {
// Handle any error from step1 through step4
}
}
scheduler(longRunningTask(initialValue));
function scheduler(task) {
var taskObj = task.next(task.value);
// 如果Generator函数未结束,就继续调用
if (!taskObj.done) {
task.value = taskObj.value
scheduler(task);
}
}
注意,上面的做法,只适合同步操作,即所有的 task 都必须是同步的,不能有异步操作。
协程
传统的编程语言,早有异步编程的解决方案(其实是多任务解决方案)。其中一种叫做协程,意思是多个线程互相协作,完成异步任务。
协程有点像函数,又有点像线程。它的运行流程大致如下。
- 第一步,协程A开始执行
- 第二步,协程A执行到一半,进入暂停,执行权转移到协程B
- 第三步,一段时间后,协程B交还执行权
- 第四步,协程A恢复执行
Generator 方法,如果要能自动执行,并且能执行有异步的操作,可以和 Promise 联合使用。