Generator

基本用法

1
2
3
4
5
6
7
8
9
10
function* testGenerator() {
yield "hello"
yield "world"
return "done"
}
let test = testGenerator()
test.next() // { value : 'hello', done : false }
test.next() // { value : 'world', done : false }
test.next() // { value : 'done', done : true }
test.next() // { value : undefined, done : true }

yield 表达式

Generator 的 next 方法运行逻辑如下:

  1. 遇到 yield 语句就暂停执行后面的操作,并将紧跟在 yield 后的表达式的值作为的对象的 value 属性值
  2. 下一次调用 next 方法时在继续往下执行,直到遇到下一条 yield 语句
  3. 如果没有遇到新的 yield 语句,就一直运行到函数结束,知道运行到 return 为止,并将 return 语句后面的表达式作为返回对象的 value 属性值
  4. 如果该函数没有 return 语句,则返回对象的 value 属性值为 undefined

yield 是惰性求值的:

1
2
3
function* gen() {
yield 123 + 456
}

以上的代码只有在 next 将指针移到这一句时才求值。
Generator 函数可以不使用 yield 语句,此时就变成了一个暂缓执行的函数,只有在调用了 next 时才执行:

1
2
3
4
5
function* f() {
console.log("run")
}
let generator = f()
setTimeout(() => generator.next(), 3000)

展开数组嵌套:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var arr = [[1, 2], 3, [4, [5]]]
var flat = function* (arr) {
var length = arr.length
for (var i = 0; i < length; i++) {
var item = arr[i]
if (typeof item !== "number") {
yield* flat(item)
} else {
yield item
}
}
}
for (var f of flat(arr)) {
console.log(f) // 1, 2, 3, 4, 5
}

next 方法的参数

next 可以带有一个参数,该参数会被当做上一条 yield 语句的返回值,这样就可以在 Generator 函数运行的不同阶段从外部向内部注入不同的值,从而调整函数的行为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function* f() {
for (var i = 0; true; i++) {
var reset = yield i
if (reset) {
i = -1
}
}
}
let generator = f()
generator.next() // { value : 0, done : false }
generator.next() // { value : 1, done : false }
generator.next() // { value : 2, done : false }
generator.next(true) // { value : 0, done : false }
generator.next() // { value : 1, done : false }
generator.next() // { value : 2, done : false }

V8 引擎直接忽略第一次使用 next 时的参数,只有第二次使用 next 开始的参数才是有效的
向内部注入值的例子:

1
2
3
4
5
6
7
8
9
10
function* dataConsumer() {
console.log("start")
console.log(`1. ${yield}`)
console.log(`2. ${yield}`)
return "result"
}
let run = dataConsumer()
run.next() // 'start'
run.next("first") // 1. first
run.next("haha") // 2. haha

Generator.prototype.throw()

throw 方法可以在函数体外抛出错误,然后在 Generator 函数体内捕获:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var g = function* () {
try {
yield
} catch (e) {
console.log("内部捕获", e)
}
}
var i = g()
i.next()
try {
i.throw("a") // 内部捕获 a
i.throw("b") // 外部捕获 b
} catch (e) {
console.log("外部捕获", e)
}

throw 方法可以接受一个参数,该参数会被 catch 语句接收:

1
2
3
4
5
6
7
8
9
10
11
var g = function* () {
try {
yield
} catch (e) {
console.log(e)
}
}
var i = g()
i.next()
i.throw(new Error("test"))
// Error: test

如果 Generator 函数内部部署了 try…catch 代码块,那么遍历器的 throw 方法抛出的错误不影响下一次遍历,否则遍历直接终止。
遍历器的 throw 与 throw 不同,后者只能被函数体外的 catch 捕获到
throw 方法被捕获后会附带执行下一条 yield 表达式,即执行一次 next 方法:

1
2
3
4
5
6
7
8
9
10
11
var gen = function* () {
try {
yield console.log("a")
} catch (e) {}
yield console.log("b")
yield console.log("c")
}
var g = gen()
g.next() // 'a'
g.throw() // 'b'
g.next() // 'c'

Generator 函数体内抛出的错误也能被函数体外的 catch 捕获:

1
2
3
4
5
6
7
8
9
10
11
12
function* foo() {
var x = yield 3
var y = x.toUpperCase()
yield y
}
var it = foo()
it.next() // { value : 3, done : false }
try {
it.next(32)
} catch (e) {
console.log(e) //TypeError
}

一旦 Generator 执行过程中抛出错误,就不会再执行下去。如果此后再调用 next,将返回一个 value 属性等于 undefined,done 属性等于 true 的对象。

Generator.prototype.return()

返回给定的值,并终结 Generator 函数的遍历:

1
2
3
4
5
6
7
8
9
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 }

如果 Generator 函数内部有 try…catch 代码块,那么 return 方法会推迟到 finally 代码块执行完成再执行。

yield*

用于在一个 Generator 函数中执行另一个 Generator 函数。

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

function* bar() {
yield "x"
yield* foo()
yield "y"
}
//等同于
function* bar() {
yield "x"
yield "a"
yield "b"
yield "y"
}

用 yield*取出嵌套数组成员:

1
2
3
4
5
6
7
8
9
10
11
function* iterTree(tree) {
if (Array.isArray(tree)) {
for (let i = 0; i < tree.length; i++) {
yield* iterTree(tree[i])
}
} else {
yield tree
}
}
const tree = ["a", ["b", "c"], ["d", "e"]]
for (let x of iterTree(tree)) console.log(x) // a b c d e

遍历完全二叉树:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function Tree(left, label, right) {
this.left = left
this.label = label
this.right = right
}
//中序遍历函数
function* inorder(t) {
if (t) {
yield* inorder(t.left)
yield t.label
yield* inorder(t.right)
}
}
//生成二叉树
function make(array) {
if (array.length === 1) return new Tree(null, array[0], null)
return new Tree(make(array[0]), array[1], make(array[2]))
}
let tree = make([[["a"], "b", ["c"]], "d", [["e"], "f", ["g"]]])
var result = []
for (let node of inorder(tree)) {
result.push(node)
}
result // ['a', 'b', 'c', 'd', 'e', 'f', 'g']

作为对象属性的 Generator 函数

1
2
3
4
5
6
7
8
9
10
11
let obj = {
*myGeneratorMethod() {
//...
},
}
//等价写法
let obj = {
myGeneratorMethod: function* () {
//...
},
}

Generator 函数 this

Generator 函数总是返回一个遍历器,ES6 规定这个遍历器是 Generator 函数的实例,它也继承了 Generator 函数的 prototype 对象上的方法。

1
2
3
4
5
6
7
function* g() {}
g.prototype.hello = function () {
return "hi"
}
let obj = g()
obj instanceof g // true
obj.hello() // 'hi'

让 Generator 函数返回一个正常对象的实例,既可以使用 next 方法,又可以获得正常的 this:

  • 方法一:
1
2
3
4
5
6
7
8
9
10
11
12
13
function* F() {
this.a = 1
yield (this.b = 2)
yield (this.c = 3)
}
var obj = {}
var f = F.call(obj) // obj 绑定this
f.next() // { value : 2, done : false }
f.next() // { value : 3, done : false }
f.next() // { value : undefined, done : true }
obj.a // 1
obj.b // 2
obj.c // 3
  • 方法二:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function* gen() {
this.a = 1
yield (this.b = 2)
yield (this.c = 3)
}
function F() {
return gen.call(gen.prototype)
}
var f = new F()
f.next() // { value : 2, done : false }
f.next() // { value : 3, done : false }
f.next() // { value : undefined, done : true }
f.a // 1
f.b // 2
f.c // 3

Generator 函数与状态机

1
2
3
4
5
6
7
8
9
10
11
12
var clock = function* () {
while (true) {
console.log("Tick")
yield
console.log("Tock")
yield
}
}
let c = clock()
c.next() // Tick
c.next() // Tock
c.next() // Tick

异步操作的同步化表达

1
2
3
4
5
6
7
8
9
10
11
12
function* loadUI() {
showLoadingScreen()
yield loadUIDataAsynchronously()
hideLoadingScreen()
}
var loader = loadUI()

//加载UI
loader.next()

//卸载UI
loader.next()

用同步方式表达 Generator 部署 AJAX 操作:

1
2
3
4
5
6
7
8
9
10
11
12
function* main() {
var result = yield request("http://some.url")
var resp = JSON.parse(result)
console.log(resp.value)
}
function request(url) {
makeAjaxCall(url, function (response) {
it.next(response)
})
}
var it = main()
it.next()

Async Generator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class A {
to = 0
constructor() {}
}

const range = {
from: 1,
to: 5,

async *[Symbol.asyncIterator]() {
for (let i = this.from, to = this.to; i < to; i++) {
await new Promise((resolve) => setTimeout(resolve, 1000))
yield i
}
},
}

;(async function () {
// for await of will call the [Symbol.asyncIterator]
for await (const value of range) console.log(value)
})()