基本概念
Generator函数是ES6提供的一种异步编程解决方案,语法行为与传统函数完全不同。
Generator函数除了是状态机,还是一个遍历器对象生成函数,所以可以把Generator函数放在Symbol.iterator属性上。
形式上,Generator函数是一个普通函数,但是有两个特征:一是function命令与函数名之间有一个星号;二是函数体内部使用yield语句定义不同的内部状态。
Generator函数是遍历器生成函数,调用Generator函数返回遍历器对象,调用遍历器对象返回成员信息,跟Iterator遍历器很相似。Generator函数是分段执行的,yield语句是暂停执行的标记,而next方法可以恢复执行。
yield表达式
yield语句就是暂停标志,遍历器对象的next方法的运行逻辑如下:
1.遇到yield语句就暂停执行后面的操作,并将紧跟在yield后的表达式的值作为返回的对象的value属性值。
2.下一次调用next方法时再继续往下执行,直到遇到吓一条yield语句。
3.如果没有再遇到新的yield语句,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值作为返回对象的value属性值。
4.如果该函数没有return语句,则返回对象的value属性值为undefined。
下面是一个Generator函数的例子
let arr = [1, [[2, 3], 4], [5, 6]]
let flat = function* (a) {
let length = a.length
for (let i = 0; i < length; i++) {
let item = a[i]
if (typeof item !== 'number') {
yield* flat(item)
} else {
yield item
}
}
}
for (let f of flat(arr)) {
console.log(f)
}
// 1 2 3 4 5 6
另外,yield表达式如果用在另一个表达式之中,必须放在圆括号里面。
function* demo () {
console.log('Hello' + yield) // 报错
console.log('Hello' + yield 123) // 报错
console.log('Hello' + (yield)) // 正确
console.log('Hello' + (yield 123)) // 正确
yield表达式用作函数参数或放在赋值表达式的右边,可以不加括号。
function* demo () {
foo(yield 'a', yield 'b') // ok
let input = yield //ok
}
与Iterator接口的关系
由于Generator函数就是遍历器生成函数,因此可以把Generator赋值给对象的Symbol.iterator属性,从而使得该对象具有Iterator接口。
let myIterator = {}
myIterator[Symbol.iterator] = function* () {
yield 1
yield 2
yield 3
}
[...myIterator] // [1,2,3]
上面代码中,Generator函数赋值给Symbol.iterator属性,从而使得myIterator对象具有了Iterator接口,可以被扩展运算符遍历。
Generator函数执行后,返回一个遍历器对象。该对象本身也具有Symbol.iterator属性,执行后返回自身。
function* gen () {
// some code
}
let g = gen()
g[Symbol.iterator]() === g
// true
yield表达式只能用在Generator函数里面,用在其他地方会报错。
next方法的参数
yield语句本身没有返回值,或者说总是返回undefined。next方法可以带有一个参数,该参数会被当作上一条yield语句的返回值。
function* f() {
for (let i = 0; true; i++) {
let reset = yield i;
if (reset) {
i = -1
}
}
}
let g = f()
console.log(g.next()) // {value: 0, done: false}
console.log(g.next()) // {value: 1, done: false}
console.log(g.next(true)) // {value: 0, done: false}
上面的代码先定义了一个可以无限运行的Generator函数f,如果next方法没有参数,每次运行到yield语句时,变量reset的值总是undefined。当next方法带有一个参数true时,当前的变量reset就被重置为这个参数(即true),因而i会等于-1,下一轮循环就从-1开始递增。
这个功能有很重要的语法意义。Generator函数从暂停状态到恢复运行,其上下文状态(context)是不变的。通过next方法的参数就有办法在Generator函数开始运行后继续向函数体内部注入值。也就是说,可以在Generator函数运行的不同阶段从外部向内部注入不同的值,从而调整函数行为。
function* foo(x) {
let y = 2 * (yield( x + 1 ))
let z = yield ( y/3 )
return ( x + y + z )
}
let a = foo(5)
a.next() // {value: 6, done: false}
a.next() // {value: NaN, done: false}
a.next() // {value: NaN, done: false}
let b = foo(5)
b.next() // {value: 6, done: false}
b.next(12) // {value: 8, done: false}
b.next(13) // {value: 42, done: false}
上面代码中运行next方法不带参数,导致yield返回值为undefined,所以导致y为NaN,z为NaN。
如果向next方法提供参数,返回结果就完全不一样了。上面的代码第一次调用b值时,返回的是6,第二次调用next方法,将上一次yield语句的值设为12,因此y的值等于24,返回8,第三次调用next方法,将上一次yield语句的值设为13,因此z等于13,return的值为42。
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
一旦next方法的返回对象的done值为true,for...of循环就会终止,且不包含该返回对象。
原生js对象没有遍历接口,无法被for...of循环,通过Generator函数为它加上这个接口后就可以用了。
function* objectEntries(obj) {
let propKeys = Reflect.ownKeys(obj)
}
for (let key of propKeys) {
yield[key, obj[key]]
}
let jane = {first: 'jane', last: 'doe'}
for (let [key, value] of objectEntries(jane)) {
console.log(`${key}: ${value}`)
}
// first: jane
// last: doe
上面代码遍历的是Iterator对象,不是jane对象,因为jane对象不具备Iterator接口,无法用for...of遍历。这时,我们通过Generator函数objectEntries为它加上遍历器接口,就可以用for...of遍历了。
function* objectEntries() {
let propKeys = Object.keys(this)
}
for (let key of propKeys) {
yield[key, this[key]]
}
let jane = {first: 'jane', last: 'doe'}
jane[Symbol.iterator] = objectEntries
for (let [key, value] of jane) {
console.log(`${key}: ${value}`)
}
// first: jane
// last: doe
Generator.prototype.throw()
Generator函数返回的遍历器对象都有一个throw方法,可以在函数体外抛出错误,然后在Generator函数体内捕获。
let g = function* () {
try {
yield
} catch (e) {
console.log('内部捕获', e)
}
}
let i = g()
i.next()
try {
i.throw('a')
i.throw('b')
} catch (e) {
console.log('外部捕获', e)
}
// 内部捕获, a
// 外部捕获,b
上面代码中,遍历器对象i连续抛出两个错误,第一个错误被Generator函数体内的catch语句捕获。第二次抛出错误,由于Generator函数内部的catch语句已经执行过了,不会再捕获到这个错误了,所以这个错误就被抛出了Generator函数体,被函数体外的catch语句捕获。不要混搅遍历器对象的throw方法和全局的throw命令。上面的错误是用遍历器对象throw方法抛出的,而不是用throw命令抛出的。后者只能被函数体外的catch语句捕获。
Generator.prototype.return()
Generator函数返回的遍历器对象还有一个return方法,可以返回给定的值,并终结Generator函数的遍历。
function* gen() {
yield 1
yield 2
yield 3
}
let g = gen()
g.next() // {value: 1, done: false}
g.return('foo') // {value: 'foo', done: true}
g.next() // {value: undefined, done: true}
如果return方法调用时不提供参数,则返回值的value属性为undefined。
如果Generator函数内部有try...finally代码块,那么return方法会推迟到finally代码块执行完再执行。
function* numbers () {
yield 1
try {
yield 2
} finally {
yield 3
}
yield 4
}
let g = numbers()
g.next() // {value: 1, done: false}
g.return(5) // {value: 2, done: false}
g.next() // {value: 3, done: false}
g.next() // {value: 5, done: true}
上面代码中,调用return方法后就开始执行finally代码块,然后等到finally代码块执行完再执行return方法。
yield*表达式
直接在Generator函数内部调用另一个Generator,是没有效果的,所以需要用到yield*语句,用来在一个Generator函数里面执行另一个Generator函数。
function* foo () {
yield 'a'
yield 'b'
}
function* bar () {
yield 'x'
yield* foo()
yield 'y'
}
// 等同于
function* bar () {
yield 'x'
yield 'a'
yield 'b'
yield 'y'
}
// 等同于
function* bar () {
yield 'x'
for (let v of foo()) {
yield v
}
yield 'y'
}
for (let i of bar()) {
console.log(i)
}
// x a b y
yield后面的Generator函数(没有return语句时)等同于在Generator函数内部部署了一个for...of循环。实际上,任何数据结构只要有Iterator接口,就可以被yield遍历。
let read = (function* () {
yield 'hello'
yield* 'hello'
})
read.next().value // hello
read.next().value // h
如果被代理的Generator函数有return语句,那么便可以向代理它的Generator函数返回数据。
function* foo () {
yield 2
yield 3
return 'foo'
}
function* bar () {
yield 1
let v = yield* foo()
console.log('v' + v)
yield 4
}
let it = bar()
it.next() // {value: 1, done: false}
it.next() // {value:2, done: false}
it.next() // v: 'foo' {value: 4, done: false}
it.next() // {value: undefined, done: true}
yield*命令可以很方便地取出嵌套数组的所有成员(类似递归)。
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
作为对象属性的Generator函数
如果一个对象的属性是Generator函数,那么可以简写成下面的形式
let obj = {
* myGeneratorFun () {
...
}
}
// 等同于
let obj = {
myGeneratorFun: function* () {
...
}
}
Generator函数this
Generator函数不是构造函数,它总是返回一个遍历器,ES6规定这个遍历器是Generator函数的实例,它也继承了Generator函数的prototype对象上的方法。
function* g() {}
g.prototype.hello = function () {
return 'hi'
}
let obj = g()
obj instanceof g // true
obj.hello() // 'hi'
上面的代码表明,Generator函数g返回的遍历器obj是g的实例,而且继承了g.prototype。但是如果把g当作普通的构造函数,则不会生效,因为g返回的总是遍历器对象,而不是this对象。
function* g() {
this.a = 11
}
let obj = g()
obj.a // undefined
Generator函数也不能跟new命令一起用,否则会报错。
function* F() {
yield this.x = 2
yield this.y = 3
}
new F() // F is not a constructor
上面代码中,new命令跟构造函数F一起使用,结果报错,因为F不是构造函数。
含义
Generator与状态机
Generator是实现状态机的最佳结构。比如,下面的clock函数就是一个状态机。
let ticking = true
let clock = function () {
if (ticking) {
console.log('tick')
} else {
console.log('tock')
}
ticking = !ticking
}
上面的clock函数一共有两种状态(tick和tock),每运行一次,就改变一次状态。这个函数用Generator实现,代码如下:
let clock = function* () {
while (true) {
console.log('tick')
yield
console.log('tock')
yield
}
}
上面代码对比我们发现,少了用来保存状态的外部变量ticking,因为Generator函数不需要外部变量保存状态,它本身就包含了状态信息,即目前是否为暂停状态。
Generator与协程
协程是一种程序运行的方式,可以理解成"协作的线程"或"协作的函数"。协程既可以用单线程实现,也可以用多线程实现。前者是一种特殊的子例程,后者是一种特殊的线程。
协程与子例程的差异
传统的"子例程"采用堆栈式"后进先出"的执行方式,只有当调用的子函数完全执行完毕,才会结束执行父函数。协程与其不同,多个线程(单线程情况下即多个函数)可以并行执行,但只有一个线程(或函数)处于正在运行的状态,其他线程(或函数)都处于暂停态,线程(或函数)之间可以交换执行权。也就是说,一个线程(或函数)执行到一半,可以暂停执行,将执行权交给另一个线程(或函数),等到稍后收回执行权时再恢复执行。这种可以并行执行、交换执行权的线程(或函数)就成为协程。
从实现上看,在内存中子例程只是用一个栈,而协程是同时存在多个栈,但只有一个栈是在运行态。也就是说,协程是以多占用内存为代价实现多任务的并行运行。
协程与普通线程的差异
不难看出,协程适用于多任务运行的环境。在这个意义上,它与普通的线程很相似,都有自己的执行上下文,可以分享全局变量。它们的不同之后在于,同一时间可以有多个线程,但是运行的协程只能有一个,其他协程都处于暂停态。此外,普通的线程是抢占式的,到底哪个线程优先得到资源,必须由运行环境决定,但是协程是合作式的,执行权由协程自己分配。
由于javascript是单线程语言,只能保持一个调用栈。引入协程以后,每个任务都可以保持自己的调用栈。这样做最大的好处就是抛出错误的时候,可以找到原始的调用栈。不至于像异步操作的回调函数那样,一旦出错原始的调用栈早就结束。
Generator函数对协程不完全实现,所以被成为"半协程",意思是只有Generator函数的调用者才能将执行权还给Generator函数。如果完全实现协程,任何函数都可以让暂停的协程继续执行。
如果将Generator函数当作协程,完全可以将多个需要互相协作的任务写成Generator函数,它们之间使用yield语句交换控制权。
应用
异步操作的同步化表达
Generator函数的暂停执行效果,意味着可以把异步操作写在yield语句里面,等到调用next方法时再往后执行。这实际上等同于不需要写回调函数了,因为异步操作的后续操作可以放在yield语句下面,反正要等到调用next方法时再执行。所以Generator函数的一个重要实际意义就是用于处理异步操作,改写回调函数。
function* loadUi () {
let result = yield loadUiAsync()
let resp = JSON.parse(result)
console.log(result.value)
}
function loadUiAsync () {
makeAjaxCall('http://some.url', function(response) {
it.next(response)
})
}
let it = loadUi()
it.next()
之前用Promise写异步需要用reject方法触发then中第一个回调函数,现在只需要调用next方法就可以执行,回调函数可以写在外面。
注意:for...of本质上是一个while循环
let it = iterateJobs(jobs)
let res = it.next()
while (!res.done) {
let result = res.value
res = it.next()
}