前言
现在差不多凌晨4点了,真是个悲伤的故事,以后我再也不在睡前玩手机了
嘛,既然睡不着就起来学习,学习让我快乐
这篇复习一下生成器和await
迭代器
首先,你要知道什么是迭代
迭代就是从一个数据集合中按照一定的顺序,不断取出数据的过程
看起来和遍历很像遍历,但是它和遍历还是有一点区别的
迭代强调的是依次取数据,并不保证取多少,也不保证把所有的数据取完
遍历强调的是要把整个数据依次全部取出
那么,迭代有什么用呢
举个例子,如果你要开发一个三方库,库里有个函数,可以打印一个集合里的元素
1 | // 打印 |
问题就来了,你要怎么遍历x呢,要是使用上面遍历数组的方法,那对Set
和Map
就不合适了,这时候就要用到一个设计模式:迭代器模式了
迭代器方式用于:提供一种方法访问一个容器元素中的各个对象,而又不暴露该对象的内部细节。
为了解决上面的问题,ES6新增了迭代协议,分为可迭代协议和迭代器协议。
其中,实现可迭代协议(Iterable 接口)的对象称为可迭代对象,实现迭代器协议的对象称为迭代器,而可迭代对象在迭代过程中会被迭代器“消费”。
迭代器协议
迭代器协议定义了产生一系列值(无论是有限个还是无限个)的标准方式。当值为有限个时,所有的值都被迭代完毕后,则会返回一个默认返回值。
这种标准方式需要满足迭代器协议的迭代器去实现,而只有实现 next()
方法的对象才能成为迭代器。
那么,来看看这个 next()
的语义是什么:next() 是一个无参函数,返回对象 IteratorReault,该对象拥有 done 和 value 两个属性:
- done,布尔值,false 表示还可以调用 next() 取得下一个值,true 代表“耗尽”
- value,当 done 为 true 时包含可迭代对象的下一个值,否则显示 undefined
在ES6中,内置了这些可迭代的对象
- String
- Array
- TypedArray
- Map
- Set
- 函数的 arguments 对象
- NodeList 等 DOM 集合类型
可以使用下面的方法来对这些对象进行迭代
- for-of
- 解构赋值
- 扩展运算符
- yield*
使用可迭代对象
你可以使用下面的方法来使用可迭代对象
1 | let arr = [1, 2]; |
这是内置的可迭代对象
实际上,如果你愿意,你也可以自己给一些对象实现可迭代协议
1 | // 为Object实现可迭代协议 |
生成器
生成器的基本使用
生成器是一个通过构造函数Generator创建的对象,生成器既是一个迭代器,同时又是一个可迭代对象
1 | new GeneratorFunction ([arg1[, arg2[, ...argN]],] functionBody) |
当然,这种方式非常繁琐,JS还有另外一种方式可以创建生成器,那就是书写生成器函数,生成器函数会返回一个生成器
1 | // 这是一个生成器函数,该函数返回一个生成器 |
- 生成器简化了迭代器的书写操作
- 生成器能方便地对生成器函数内部的逻辑进行控制。在生成器函数内部,通过
yield
或yield*
,将当前生成器函数的控制权移交给外部,外部通过调用生成器的next
或throw
或return
方法将控制权返还给生成器函数,并且还能够向其传递数据。
举个例子,上面的自定义迭代器可以写成
1 | // 为Object实现可迭代协议 |
是不是简单了许多
1 | // 移交生成器控制权并和外部交互 |
运行结果是这样的
1 | function* genDemo() { |
执行结果如下
生成器的异步 / co库
因为生成器有下面的特点
- 可以对控制权进行移交
- 可以对外部传递数据
- 外部可以传入数据给生成器
我们就可以用它来处理异步操作,最常用的库应该就是co
了,我曾经试着仿写了一下co
库,代码如下
1 | function run(generatorFunc, data) { |
思路非常简单,就是处理错误要包几层,这里就说下大概的思路(不出错的情况)就可以了
- 创建生成器(
gen
) - 对生成器进行迭代,拿到每次迭代出的返回值
- 返回值是
Promise
,使用Promise
的then
方法注册回调,让Promise
状态变化后再继续迭代 - 返回值不是
Promise
,把返回值通过gen.next()
传回去
- 返回值是
- 直到迭代结束
思路还是非常简单的,虽然代码有点复杂就是了www
生成器和协程
看了这么多,你应该已经知道,生成器函数是可以暂停执行和恢复执行的。它的运行规则如下
- 在生成器函数内部执行一段代码,如果遇到 yield 关键字,那么 JavaScript 引擎将返回关键字后面的内容给外部,并暂停该函数的执行。
- 外部函数可以通过 next 方法恢复函数的执行。
那么它的具体原理是什么呢
要搞懂它的原理,那你首先要了解协程的概念。
协程是一种比线程更加轻量级的存在。你可以把协程看成是跑在线程上的任务,一个线程上可以存在多个协程,但是在线程上同时只能执行一个协程,比如当前执行的是 A 协程,要启动 B 协程,那么 A 协程就需要将主线程的控制权交给 B 协程,这就体现在 A 协程暂停执行,B 协程恢复执行;同样,也可以从 B 协程中启动 A 协程。通常,如果从 A 协程启动 B 协程,我们就把 A 协程称为 B 协程的父协程。
正如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程。最重要的是,协程不是被操作系统内核所管理,而完全是由程序所控制。这样带来的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源。
你可以参照下面的“协程执行流程图”分析上面的代码是怎么运行的
可以看出来协程的规则如下
- 通过调用生成器函数 genDemo 来创建一个协程 gen,创建之后,gen 协程并没有立即执行。
- 要让 gen 协程执行,需要通过调用 gen.next。
- 当协程正在执行的时候,可以通过 yield 关键字来暂停 gen 协程的执行,并返回主要信息给父协程。
- 如果协程在执行期间,遇到了 return 关键字,那么 JavaScript 引擎会结束当前协程,并将 return 后面的内容返回给父协程。
值的一提的是,当在 gen 协程中调用了 yield 方法时,JavaScript 引擎会保存 gen 协程当前的调用栈信息,并恢复父协程的调用栈信息。同样,当在父协程中执行 gen.next 时,JavaScript 引擎会保存父协程的调用栈信息,并恢复 gen 协程的调用栈信息。
相信看到这里你已经明白了生成器的原理了,就是协程的切换和调度
async/await
终于到了我们最爱的async/await
了,async/await
你可以理解成Promise
+ 生成器 + co
的语法糖
比如下面的代码
1 | function * requestData() { |
写成async/await
的形式就是
1 | async function requestData() { |
看出来了吧,简直一模一样
总结一下就是
- async 是一个通过异步执行并隐式返回 Promise 作为结果的函数。
- await 会使用
Promise.resolve
包裹输入
后记
唔,写完差不多六点了,困了,该去躺躺了orz