Generator 定义

Generator 是隐藏类 Iterator 的子类,Generator 对象是一个 JavaScript 的标准内置对象,它由生成器函数返回并且它符合可迭代协议和迭代器协议

生成器函数 形如 function* name() {},它内部可以包含 yield 表达式,具体功能下文再述

可迭代协议:简单说就是一个对象实现了 [Symbol.iterator]() 方法,这个方法无参,返回一个符合迭代器协议的对象

迭代器协议:定义了产生一系列值(无论是有限个还是无限个)的标准方式,当值为有限个时,所有的值都被迭代完毕后,则会返回一个默认返回值。最简的迭代器对象必须实现 next() 方法,该方法无参或接受一个参数,返回一个符合 IteratorResult 接口的对象。IteratorResult 简单地描述一下就是:

1
2
3
4
interface IteratorResult {
done?: boolean;
value?: any;
}

其中 done 标识是否完成了迭代

写一个简单的符合可迭代协议和迭代器协议的对象就是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const myIteratorObj = {
i: 3,
next() {
return {
value: this.i,
done: this.i-- === 0
}
},
[Symbol.iterator]() {
return this
}
}

console.log([...myIteratorObj]) // [3, 2, 1]

而普通的对象被数组重组时会抛错 TypeError: object is not iterable

Generator 生成器函数

回到 Generator,它有什么用?这就得继续讨论生成器函数

生成器函数中的 yield 表达式会使生成器函数返回的 Generator 对象执行 next() 方法后暂停执行生成器函数主体,由此可以用来异步编程。对于上文的 myIteratorObj,可以由生成器改写为:

1
2
3
4
5
6
7
const myGenerator = function* () {
yield 3
yield 2
yield 1
}
const myIterator = myGenerator()
console.log([...myIterator]) // [3, 2, 1]

其中的 yield 关键字:

  • 只能出现在 Generator 函数中
  • 只能用来暂停和回复生成器函数

因此 yield 不可能存在于 Array.prototype.forEach() 等方法中,即以下用法是语法错误的:

1
2
3
4
5
6
const myGenerator = function* () {
const items = [1, 2, 3]
items.forEach((item) => {
yield item
})
}

而应该为:

1
2
3
4
5
6
const myGenerator = function* () {
const items = [1, 2, 3]
for (let i = 0; i < items.length; i++) {
yield items[i]
}
}

符合迭代器协议的对象的 next() 方法,也可以接受一个参数,该参数会作为上一个 yield 表达式的返回值传入,并覆盖上一个 yield 表达式的返回值,比如:

1
2
3
4
5
6
7
8
function* generator() {
let first = yield 1
yield first + 2
}

let iterator = generator()
console.log(iterator.next()) // {value: 1, done: false}
console.log(iterator.next(9)) // {value: 11, done: false}

上述代码第二次执行 next() 并传入 9,得到的 value 即为 11,而不是 3

应用场景

生成器函数的 MDN 文档中提到了生成器函数可以解决回调地狱问题,比如顺序读取文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function readFileByCallback() {
const fs = require("fs")
const files = ["/path-to/1.json", "/path-to/2.json", "/path-to/3.json"]
fs.readFile(files[0], function (err, data) {
console.log(data.toString())
fs.readFile(files[1], function (err, data) {
console.log(data.toString())
fs.readFile(files[2], function (err, data) {
console.log(data.toString())
})
})
})
}

readFileByCallback()

三层回调恐怖如斯,可以使用 Generator 改写为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function* readFileByGenerator() {
const fs = require("fs")
const files = ["/path-to/1.json", "/path-to/2.json", "/path-to/3.json"]
function readFile(filename) {
fs.readFile(filename, function (err, data) {
console.log(data.toString())
f.next()
})
}
for(let i = 0; i < files.length; i++) {
yield readFile(files[i])
}
}

const f = readFileByGenerator()
f.next()

但这种写法将 f 这个外部变量高耦合到 readFileByGenerator() 方法中,还是不够优雅,于是需要用到以下的 Thunk 函数

Thunk 函数

首先了解一下两种求值策略

  1. 传值调用:传入所需参数进行计算
  2. 传名调用:传入计算方法、参数等进行计算

Thunk 函数是传名调用的实现方式之一,可以实现自动执行 Generator 函数。示例:

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
const fs = require("fs")
const Thunk = function (fn) {
return function (...args) {
return function (callback) {
return fn.call(this, ...args, callback)
}
}
}

const readFileThunk = Thunk(fs.readFile)

// 接受一个 Generator 函数,并自动执行
function run(fn) {
var gen = fn()
function next(err, data) {
var result = gen.next(data)
if (result.done) return
result.value(next)
}
next()
}

const g = function* () {
const files = ["/path-to/1.json", "/path-to/2.json", "/path-to/3.json"]
for(let i = 0; i < files.length; i++) {
const s1 = yield readFileThunk(files[i])
console.log(s1.toString())
}
}

run(g)

假设上述代码中的 Thunk 中的两个匿名函数从外到里依次为 thunk1thunk2,则整段代码的执行过程如下:

thunk-trace

上图对应的 trace 的生成代码见 thunk.js

上图对应的代码见 thunk-trace

其中,同步任务在 run 方法执行后已经结束,后续的均为 readFile 或者 Generator 函数的其他异步的任务

需要注意的是,在调用 readFileThunk 后,返回的并不是读取文件的值,而是返回了 Thunk 中最内层的匿名函数 thunk2;接下来调用 result.value(next) 时,才真正执行读取文件的操作,并且读取文件后的内容传递给了 next(err, data) 中的 data,由于向 gen.next() 传递参数会被当作上一个 yield 表达式的返回值,因此 s1 变成了读取文件的内容 data 并被输出,其他 yield 同此

yield 暂停生成器执行机制的实现原理

一个线程中可以存在多个协程,但同时只能执行一个,Generator 函数是协程在 ES6 的实现。当遇到 yield 表达式,则挂起 x 协程,交给其他协程,next() 唤醒 x 协程

补充

yield*

类似 yield 的机制,执行到 yield* 时,把执行权交给紧跟的生成器。可以复用生成器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function* generator1() {
yield 1
yield 2
}

function* generator2() {
yield 100
yield* generator1()
yield 200
}

let g2 = generator2()

g2.next() // {value:100, done: false}
g2.next() // {value:1, done: false}
g2.next() // {value:1, done: false}
g2.next() // {value:200, done: false}
g2.next() // {value:undefined, done: true}

return(param) 和 throw(param)

实际的 Generator 对象还包含 return()throw()

顾名思义,return() 会提前中止 Generator 的执行;而 throw() 会主动引起报错,如果生成器主体没有捕获改错误,则会继续上抛错误,不传入任何异常时捕获到的错误是一个 undefined

生成器中的 return

在生成器主体中提前调用 return,则会提前终止迭代,迭代结果的 done 会被置为 true