本章介绍ES6生成器,是一种顺序的、看似同步的异步流程表达风格。
1. 打破完整运行
生成器是一种特殊的函数类型,它不会一口气从头运行到结束。生成器内可有多个暂停点,也就是yield,有yield的地方它会主动放弃控制,暂停运行。
作者举了个例子说明了生成器运行的过程。生成器的运行,是用一个迭代器来控制,调用一次迭代器.next()可控制生成器运行“一段”,多次调用next直到生成器运行到结束。
1.1输入和输出
生成器函数是一个特殊的函数,但它仍有函数的一些基本特性,比如可以接收参数、可以返回值。
此外,生成器还有独特的消息输入输出能力,yield和next()之间可以传递值。
1.2 多个迭代器
迭代器控制生成器的时候,实际上是在控制一个生成器的实例,所以同一个生成器的多个实例可以独立运行,甚至可以彼此交互(把迭代器A的next()取到的值传到迭代器B的next方法里当参数)。
另外,多个生成器在共享作用域上并发运行根据调用顺序的不同可有多种复杂的运行结果(就像多线程语言的并发)。
2.生成器产生值
这章主要讲的不是生成器怎么产生值,但是生成器的一个用途是产生值,而且这也是它得名的原因。所以还是要聊聊生成器怎么产生值,说到产生值,就不得不提一下迭代器。
2.1 生产者与迭代器
在很多场景下,都需要一个东西能生产出一系列值给消费方,这一系列值中每一个值都与前一个有特定的关系,并且每次只生产一个值,而不是一次性生产出整一系列的值。(因为对生产者来说,一个一个随用随生产,不容易造成资源泄漏。尤其是,那个值可能不是简单的数字或字符串,它可能是一个大文件。)
比如说,想从数据源生成记录,我们就需要一个对象专门读取记录。
这个任务是一种非常通用的设计模式,通常通过迭代器来解决。迭代器是一个定义良好的接口,用于从一个生产者一步步得到一系列值。JavaScript迭代器的接口与多数语言类似,就是每次想要从生产者得到下一个值的时候调用next()。
ES6的for..of可以迭代实现了迭代器接口的东西(比如Array)。
来自MDN的资料:
迭代协议:包括 可迭代协议 和 迭代器协议
可迭代协议:js对象通过遵守可迭代协议来定制自己的迭代行为。有些内置类型已经具有默认的迭代行为,比如Array和Map。
怎么遵守可迭代协议来定义迭代行为呢? 答:这个对象须具有一个属性,键为Symbol.iterator,值为一个无参函数(可以是普通函数,也可以是生成器函数),这个函数需要返回一个符合迭代器协议的对象。
迭代器协议:一个对象怎么样通过遵守迭代器协议来成为迭代器? 答:它得具有属性next,值为无参或单参函数,返回一个对象,这个对象有属性done和value,其中done的值应当是布尔值。
通常一个对象会同时满足可迭代协议和迭代器协议,很少有只满足其一的。(比如,生成器对象,既是可迭代对象,也是迭代器)
2.2 iterable
iterable就是一个支持迭代的对象,什么是支持迭代的:1.对象的Symbol.iterator属性是一个函数 2.这个函数会返回一个迭代器(一个具有next方法的对象)。
for..of会自动调用iterable的Symbol.iterator,我们也可以手动去调用。例:
var arr = [1, 2]
var it = arr[Symbol.iterator]()
it.next().value // 1
it.next().value // 2
2.3 生成器迭代器
生成器可以看作是一个值的生产者,通过迭代器的next()方法,每调一次取出一个值。
生成器不是iterable,但是很类似,因为通过执行生成器就能得到一个迭代器。
生成器的迭代器也是一个iterable,一般来说它的Symbol.iterator返回它自身。
如何终止生成器:
for..of循环结束(包括因为break、return、未捕获的异常 和 正常循环结束)时,for..of会向迭代器发送信号使其终止。
此外,手动调用it.return(值)也能让迭代器终止。
其它迭代器可能没有return或者有return但逻辑不一样,反正生成器的迭代器的return方法被调用后,会让生成器直接走到return语句,且返回值等于return方法接收的参数。
调用return后,生成器的迭代器就被设置为done: true,所以此时for..of不需加break也会停止迭代。
可以在生成器内写好try..finally,这样,即使在try代码块里被return掉了,finally代码块也会被执行,finally代码块里可以写一些清理资源(如数据库连接)的逻辑。
在控制台试了一下,可以做到迭代器怎么throw,生成器都不结束。似乎再改改代码,还能让迭代器无论怎么return,生成器都不结束。
3.异步迭代生成器
生成器可以用于表达异步逻辑,它可以把异步逻辑表达得很直观、很顺序,像同步逻辑一样,只要在需要异步操作结果的时候yield一下,暂停执行,等外面的迭代器在取到异步结果以后,再推动生成器继续执行。
因为生成器的yield暂停特性,迭代器还能在异步操作出错时,把错误抛回到生成器中,让生成器中看似同步的catch块捕捉到异步错误。
生成器除了可以接收迭代器抛进来的错误,自身也能向外抛出错误。
迭代器向生成器抛入的错误如果没有被生成器处理,错误就会被抛回到外面,迭代器那边的代码就得处理这个错误。
总之,有了生成器,异步逻辑可以被写成类似同步的、顺序的代码,可读性得到了巨大的提升。
4.生成器+Promise
生成器可以使异步逻辑(包括异步错误处理)被看似同步的代码表达,再加上Promise的可信任性和可组合性,这两者的结合是用于表达异步逻辑的很完美的方案。
(实际上,这两者结合,就是async方法,async方法可以理解为是:专门在异步操作时yield的生成器)
5.生成器委托
function *foo() {
yield *bar()
}
生成器bar的迭代器可以被委托给生成器foo。生成器委托,就像普通函数之间的互相调用,通过互相调用,函数才能灵活组合。但是生成器是一断一断的,需要迭代器控制它往下走,所以不能用普通函数的调用方式,所以就有了生成器委托。
tips:yield委托不仅可以委托生成器迭代器,普通迭代器也能委托。
6.生成器并发
就像Promise可以并发一样,生成器也可以看作是一种表达异步步骤的“段落”,通过一些工具包裹,它们也能并发。(async方法就能并发,因为它是被包裹过的生成器,调用async方法能得到一个Promise。不过如果手工去包裹Promise可以更“私人订制”)
7.形实转换程序
前面说的都是让生成器yield出Promise,生成器也可以yield出一个thunk。什么是thunk?可以理解为,是一个函数,封装了对另一个函数的调用。
在表达异步流程的时候,异步请求就可以被thunk化,封起来。
所以在一个生成器里,需要等待异步操作的时候,yield暂停下来,可以是为了一个Promise暂停,也可以是为了一个thunk暂停。
作者讲到这一点,主要是为了讨论,如果生成器可以为了thunk暂停,那么前面说的专门用来处理“生成器+Promise”的工具应该怎么写?这个工具应当同时支持Promise和thunk。
(不过我觉得我应该不会需要接触thunk的)
8.ES6之前的生成器
本节作者主要讲,如何用ES5语法“实现”生成器。
首先考虑“成品”应有的“外观”:有一个函数,它是我们的“手工生成器”。执行手工生成器,应该得到一个“迭代器”,也就是一个对象,它有next和throw方法,调用后能得到{value: 值, done: 布尔值}
这样的对象。
我想象了一下这个手工生成器的样子,大概是这样:
function myGenerator(入参) {
return {
next: function() {
第一段代码
this.next = function() {
第二段代码
this.next = function() { 第三段代码…… }
return {
value: 第二段代码结束后要yield出去的值,
done: false
}
}
return {
value: 第一段代码结束后要yield出去的值,
done: false
}
}
}
}
但是后面往下看,发现我忘了考虑throw的情况。“外面“不仅可能调it.next(),还可能调it.throw()。
作者介绍的写法是给生成器的逻辑标记状态点。
function* A() {
// 点1
xxx;
try {
yield y;
// 点2
zzz;
} catch {
// 点3
vvv;
}
www;
}
第一次调用it.next(),是从点1开始执行。然后如果再调一次it.next(),是从点2继续执行,反之如果调的是it.throw(),则从点3开始执行。
按作者思路再来写一下这个手工生成器:
function myGenerator(入参) {
let status = null
// 一些变量声明,给下面的代码片段用
function process() {
switch (status) {
case 1:
xxx;
return { value: y, done: false }
case 2:
zzz;
www;
return { value: undefined, done: true }
case 3:
vvv;
www;
return { value: undefined, done: true }
}
}
return {
next: function (v) {
switch (status) {
case null:
status = 1;
return process(v)
case 1:
status = 2;
return process(v)
}
},
throw: function (err) {
switch (status) {
case 1:
status = 3
return process(err)
default:
throw err;
// 我没考虑到如果it不在我期望的地方调throw该咋办,回顾的时候发现作者的答案里有这部分逻辑,所以补上。
// default表示it不在能被捕获到错误的地方抛出错误,所以错误不会被生成器处理,会被抛回到外面
}
}
}
}
上面这个例子只是一个实践,不能用于真正的生产,因为太手工了,好累。如果真的要在ES5环境下用生成器,可以借助现成的代码转换工具。
9.小结
生成器是一个新的函数类型,可以通过yield语句暂停自身,它的暂停和恢复是合作式的而不是抢占式的。
yeild和next()除了可以控制流程的暂停和结束,还能进行双向消息传递。
在异步流程控制方面,生成器的优点在于可以以同步的、顺序的方式表达异步逻辑,提高了代码可读性。