前言

现在差不多凌晨4点了,真是个悲伤的故事,以后我再也不在睡前玩手机了

嘛,既然睡不着就起来学习,学习让我快乐

这篇复习一下生成器和await

迭代器

首先,你要知道什么是迭代

迭代就是从一个数据集合中按照一定的顺序,不断取出数据的过程

看起来和遍历很像遍历,但是它和遍历还是有一点区别的

迭代强调的是依次取数据,并不保证取多少,也不保证把所有的数据取完

遍历强调的是要把整个数据依次全部取出

那么,迭代有什么用呢

举个例子,如果你要开发一个三方库,库里有个函数,可以打印一个集合里的元素

1
2
3
4
// 打印
function foo(x) {
// 对x里的元素进行打印
}

问题就来了,你要怎么遍历x呢,要是使用上面遍历数组的方法,那对SetMap就不合适了,这时候就要用到一个设计模式:迭代器模式了

迭代器方式用于:提供一种方法访问一个容器元素中的各个对象,而又不暴露该对象的内部细节

为了解决上面的问题,ES6新增了迭代协议,分为可迭代协议迭代器协议

其中,实现可迭代协议(Iterable 接口)的对象称为可迭代对象,实现迭代器协议的对象称为迭代器,而可迭代对象在迭代过程中会被迭代器“消费”。

迭代器协议

迭代器协议定义了产生一系列值(无论是有限个还是无限个)的标准方式。当值为有限个时,所有的值都被迭代完毕后,则会返回一个默认返回值。

这种标准方式需要满足迭代器协议的迭代器去实现,而只有实现 next() 方法的对象才能成为迭代器。

那么,来看看这个 next() 的语义是什么:next() 是一个无参函数,返回对象 IteratorReault,该对象拥有 donevalue 两个属性:

  • done,布尔值,false 表示还可以调用 next() 取得下一个值,true 代表“耗尽”
  • value,当 done 为 true 时包含可迭代对象的下一个值,否则显示 undefined

在ES6中,内置了这些可迭代的对象

  • String
  • Array
  • TypedArray
  • Map
  • Set
  • 函数的 arguments 对象
  • NodeList 等 DOM 集合类型

可以使用下面的方法来对这些对象进行迭代

  • for-of
  • 解构赋值
  • 扩展运算符
  • yield*

使用可迭代对象

你可以使用下面的方法来使用可迭代对象

1
2
3
4
5
6
7
8
let arr = [1, 2];
// 创建迭代器
let iter = arr[Symbol.iterator]();
// 执行迭代器
console.log(iter.next()); // {value: 1, done: false}
console.log(iter.next()); // {value: 1, done: false}
console.log(iter.next()); // {value: undefined, done: true}
console.log(iter.next()); // {value: undefined, done: true}

这是内置的可迭代对象

实际上,如果你愿意,你也可以自己给一些对象实现可迭代协议

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 为Object实现可迭代协议
Object.prototype[Symbol.iterator] = function () {
let keys = Object.keys(this);
let index = 0;
let length = keys.length;
// 返回一个迭代器
return {
next() {
console.log("rua")
let inx = index++;
return {
done : inx >= length,
value : keys[inx]
}
}
}
}

let obj = {
name : "sena",
age : 16,

}

console.log(...obj); // name age

生成器

生成器的基本使用

生成器是一个通过构造函数Generator创建的对象,生成器既是一个迭代器,同时又是一个可迭代对象

1
new GeneratorFunction ([arg1[, arg2[, ...argN]],] functionBody)

当然,这种方式非常繁琐,JS还有另外一种方式可以创建生成器,那就是书写生成器函数,生成器函数会返回一个生成器

1
2
3
4
// 这是一个生成器函数,该函数返回一个生成器
function* method(){

}
  • 生成器简化了迭代器的书写操作
  • 生成器能方便地对生成器函数内部的逻辑进行控制。在生成器函数内部,通过 yieldyield* ,将当前生成器函数的控制权移交给外部,外部通过调用生成器的 nextthrowreturn 方法将控制权返还给生成器函数,并且还能够向其传递数据。

举个例子,上面的自定义迭代器可以写成

1
2
3
4
5
6
7
8
9
// 为Object实现可迭代协议
Object.prototype[Symbol.iterator] = function* () {
let keys = Object.keys(this);
let index = 0;
let length = keys.length;
while (index < length) {
yield keys[index++];
}
}

是不是简单了许多

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 移交生成器控制权并和外部交互
function * func(num) {
console.log("rua", num)
let num1 = yield num + 1;
console.log("rua", num1);
let num2 = yield num1 + 2;
console.log("rua", num2);
return num2 + 3;
}

const generator = func(0);
console.log("qaq", generator.next(10));
console.log("qaq", generator.next(20));
console.log("qaq", generator.next(30));

运行结果是这样的

image-20210208045622168

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* genDemo() {
console.log("开始执行第一段")
yield 'generator 2'

console.log("开始执行第二段")
yield 'generator 2'

console.log("开始执行第三段")
yield 'generator 2'

console.log("执行结束")
return 'generator 2'
}

console.log('main 0')
let gen = genDemo()
console.log(gen.next().value)
console.log('main 1')
console.log(gen.next().value)
console.log('main 2')
console.log(gen.next().value)
console.log('main 3')
console.log(gen.next().value)
console.log('main 4')

执行结果如下

image-20210208050944546

生成器的异步 / co库

因为生成器有下面的特点

  • 可以对控制权进行移交
  • 可以对外部传递数据
  • 外部可以传入数据给生成器

我们就可以用它来处理异步操作,最常用的库应该就是co了,我曾经试着仿写了一下co库,代码如下

https://www.sakura-snow.com/archives/160

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
function run(generatorFunc, data) {
// 获得生成器
const generator = generatorFunc(data);
// 执行next方法获取结果
let result = execute(generator);
return new Promise((resolve, reject) => {
// 处理结果
handleResult(generator, result, resolve, reject);
});
}
function handleResult(generator, result, resolve, reject) {
// 如果result.done是真,证明已经迭代完了,而且第一次为true时的value是生成器函数的返回值
// 通过resolve可以把返回值传给run函数外的then
if (result.done){
resolve(result.value);
return;
}
// 如果值是个promise
if (isPromise(result.value)){
// 把next()放到then里面执行
result.value.then((data) => {
result = execute(generator, data);
handleResult(generator, result, resolve, reject);
}, (err) =>
// 当返回的promise执行reject时
{
// 在外面包一层try catch, 如果生成器函数里没有写try catch, 就会在这里捕获error
try {
// 把错误抛入生成器
result = throwError(generator, err);
// 如果错了try catch就继续处理
handleResult(generator, result, resolve, reject);
}catch (e) {
// 如果在这里接收到错误, 证明生成器里的错误没有被捕获, 执行绑定的reject
reject(e);
}
})
} else {
// 如果没有结束, 就继续执行, 同时把上一个next返回的value传回去
result = execute(generator, result.value);
// 这里的generator, resolve, reject都是一层一层往下传不会变的
// 因为要执行的是同一个生成器, 要接收返回值和错误原因的也是同一个函数, 都是要传给run函数返回的promise绑定的then
handleResult(generator, result, resolve, reject);
}
}
// 就是执行next方法
function execute(generator, data) {
return generator.next(data);
}
// 用于把错误抛入生成器
function throwError(generator, error) {
return generator.throw(error);
}
// 用于判断返回值是不是promise
function isPromise(obj) {
return 'function' == typeof obj.then;
}

思路非常简单,就是处理错误要包几层,这里就说下大概的思路(不出错的情况)就可以了

  • 创建生成器(gen)
  • 对生成器进行迭代,拿到每次迭代出的返回值
    • 返回值是Promise,使用Promisethen方法注册回调,让Promise状态变化后再继续迭代
    • 返回值不是Promise,把返回值通过gen.next()传回去
  • 直到迭代结束

思路还是非常简单的,虽然代码有点复杂就是了www

生成器和协程

看了这么多,你应该已经知道,生成器函数是可以暂停执行和恢复执行的。它的运行规则如下

  • 在生成器函数内部执行一段代码,如果遇到 yield 关键字,那么 JavaScript 引擎将返回关键字后面的内容给外部,并暂停该函数的执行。
  • 外部函数可以通过 next 方法恢复函数的执行。

那么它的具体原理是什么呢

要搞懂它的原理,那你首先要了解协程的概念。

协程是一种比线程更加轻量级的存在。你可以把协程看成是跑在线程上的任务,一个线程上可以存在多个协程,但是在线程上同时只能执行一个协程,比如当前执行的是 A 协程,要启动 B 协程,那么 A 协程就需要将主线程的控制权交给 B 协程,这就体现在 A 协程暂停执行,B 协程恢复执行;同样,也可以从 B 协程中启动 A 协程。通常,如果从 A 协程启动 B 协程,我们就把 A 协程称为 B 协程的父协程

正如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程。最重要的是,协程不是被操作系统内核所管理,而完全是由程序所控制。这样带来的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源。

你可以参照下面的“协程执行流程图”分析上面的代码是怎么运行的

img

可以看出来协程的规则如下

  • 通过调用生成器函数 genDemo 来创建一个协程 gen,创建之后,gen 协程并没有立即执行。
  • 要让 gen 协程执行,需要通过调用 gen.next。
  • 当协程正在执行的时候,可以通过 yield 关键字来暂停 gen 协程的执行,并返回主要信息给父协程。
  • 如果协程在执行期间,遇到了 return 关键字,那么 JavaScript 引擎会结束当前协程,并将 return 后面的内容返回给父协程。

值的一提的是,当在 gen 协程中调用了 yield 方法时,JavaScript 引擎会保存 gen 协程当前的调用栈信息,并恢复父协程的调用栈信息。同样,当在父协程中执行 gen.next 时,JavaScript 引擎会保存父协程的调用栈信息,并恢复 gen 协程的调用栈信息。

img

相信看到这里你已经明白了生成器的原理了,就是协程的切换和调度

async/await

终于到了我们最爱的async/await了,async/await你可以理解成Promise + 生成器 + co的语法糖

比如下面的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function * requestData() {
let name = yield api1();
console.log(name); // "sena"
return {
name
};
}
function api1() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("sena");
}, 1000);
})
}
run(requestData).then((res) => {
console.log(res);
});

写成async/await的形式就是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
async function requestData() {
let name = await api1();
console.log(name); // "sena"
return {
name
};
}
function api1() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("sena");
}, 1000);
})
}
requestData().then((res) => {
console.log(res);
});

看出来了吧,简直一模一样

总结一下就是

  • async 是一个通过异步执行并隐式返回 Promise 作为结果的函数。
  • await 会使用Promise.resolve 包裹输入

后记

唔,写完差不多六点了,困了,该去躺躺了orz

FAQ & Ref

浏览器工作原理与实践

进阶 Javascript 生成器

迭代器和生成器