Generator 简介
Generator 定义
Generator 是隐藏类 Iterator 的子类,Generator 对象是一个 JavaScript 的标准内置对象,它由生成器函数返回并且它符合可迭代协议和迭代器协议
生成器函数 形如 function* name() {}
,它内部可以包含 yield
表达式,具体功能下文再述
可迭代协议:简单说就是一个对象实现了 [Symbol.iterator]()
方法,这个方法无参,返回一个符合迭代器协议的对象
迭代器协议:定义了产生一系列值(无论是有限个还是无限个)的标准方式,当值为有限个时,所有的值都被迭代完毕后,则会返回一个默认返回值。最简的迭代器对象必须实现 next()
方法,该方法无参或接受一个参数,返回一个符合 IteratorResult
接口的对象。IteratorResult
简单地描述一下就是:
1 | interface IteratorResult { |
其中 done
标识是否完成了迭代
写一个简单的符合可迭代协议和迭代器协议的对象就是:
1 | const myIteratorObj = { |
而普通的对象被数组重组时会抛错 TypeError: object is not iterable
Generator 生成器函数
回到 Generator,它有什么用?这就得继续讨论生成器函数
生成器函数中的 yield
表达式会使生成器函数返回的 Generator 对象执行 next()
方法后暂停执行生成器函数主体,由此可以用来异步编程。对于上文的 myIteratorObj
,可以由生成器改写为:
1 | const myGenerator = function* () { |
其中的 yield
关键字:
- 只能出现在 Generator 函数中
- 只能用来暂停和回复生成器函数
因此 yield
不可能存在于 Array.prototype.forEach()
等方法中,即以下用法是语法错误的:
1 | const myGenerator = function* () { |
而应该为:
1 | const myGenerator = function* () { |
符合迭代器协议的对象的 next()
方法,也可以接受一个参数,该参数会作为上一个 yield 表达式的返回值传入,并覆盖上一个 yield 表达式的返回值,比如:
1 | function* generator() { |
上述代码第二次执行 next() 并传入 9
,得到的 value 即为 11
,而不是 3
应用场景
生成器函数的 MDN 文档中提到了生成器函数可以解决回调地狱问题,比如顺序读取文件:
1 | function readFileByCallback() { |
三层回调恐怖如斯,可以使用 Generator 改写为:
1 | function* readFileByGenerator() { |
但这种写法将 f
这个外部变量高耦合到 readFileByGenerator()
方法中,还是不够优雅,于是需要用到以下的 Thunk 函数
Thunk 函数
首先了解一下两种求值策略
- 传值调用:传入所需参数进行计算
- 传名调用:传入计算方法、参数等进行计算
Thunk 函数是传名调用的实现方式之一,可以实现自动执行 Generator 函数。示例:
1 | const fs = require("fs") |
假设上述代码中的 Thunk
中的两个匿名函数从外到里依次为 thunk1
、thunk2
,则整段代码的执行过程如下:
上图对应的 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 | function* generator1() { |
return(param) 和 throw(param)
实际的 Generator 对象还包含 return()
和 throw()
顾名思义,return()
会提前中止 Generator 的执行;而 throw()
会主动引起报错,如果生成器主体没有捕获改错误,则会继续上抛错误,不传入任何异常时捕获到的错误是一个 undefined
生成器中的 return
在生成器主体中提前调用 return,则会提前终止迭代,迭代结果的 done
会被置为 true