持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第4天,点击查看活动详情
前言
在上一篇文章中,我们详细的讨论了什么是 迭代器,这篇文章依然从迭代器出发,进而展开地介绍什么是 生成器,以及生成器的高级应用场景
1. 迭代器回顾
我们说一个对象实现了 Symbol.iterator
可迭代协议,那么这个调用这个对象的 Symbol.iterator
方法就可以返回一个独立的迭代器
const obj = {
[Symbol.iterator]() {
return {
next() {
// ...
return { done: true, value: undefined };
},
};
},
};
for (const i of obj) {
console.log(i);
}
只要实现了 1迭代器
接口我们可以改造任意类型数据结构将其转换为可迭代对象
我们得花费大量的代码去构造一个可迭代对象,而在 ES6 中,除了实现对象中的 Symbol.iterator
这种方式,我们还可以使用 ES6 提供的生成器,简化代码
下面是一个简单的生成器的例子
function* generatorFn() {
yield 1;
yield 2;
yield 3;
}
for (const i of generatorFn()) {
console.log(i);
}
// 1
// 2
// 3
对于 function
关键词后面这种带 *
的函数我们就可以说它是生成器了,下面我们就对这个例子详细的展介绍什么是 生成器
2. 生成器也是迭代器?
我们知道,能被 for~of 循环
的对象一定实现了 Symbol.iterator
接口,在上面的这个例子中,我们调用生成器函数,生成器函数返回值竟然也可以被 for~of 循环
2.1 生成器函数的返回值
const generator = generatorFn();
生成器函数返回值:
我们执行生成器函数,返回值会产生一个生成器对象,在这个对象的原型上我们看到了 Symbol.iterator
接口,所以这个对象能被 迭代
3. 生成器基本使用
生成器的形式就是一个工厂函数,在普通函数的 function
关键字的前面添加一个 *
,就可以定义生成器了,调用生成器的工厂函数就会一个独立的 生成器对象
生成器对象:
生成器对象被构造出来的时候处于 suspended
暂停执行的状态,我们调用一次 next
方法,就会开始执行生成器,next
方法来源于 Generator
,注意与 Symbol.iterator
的 next
区别
函数体没有 yield
:
function* generatorFn() { }
const generator = generatorFn();
generator.next(); // {value: undefined, done: true}
next
函数的返回值为 {value: any, done: boolean}
; 当 done
为 true
的时候,生成器对象的状态为 closed
,表示整个生成器对象 声明周期 的结束。我们为什么提到了 声明周期 的关键字,下面的代码能够很好的说明
函数体有 yield
:
function* generatorFn() {
yield 1;
yield 2;
yield 3;
}
const generator = generatorFn();
generator.next(); // {value: 1, done: false}
generator.next(); // {value: 1, done: false}
generator.next(); // {value: 1, done: false}
generator.next(); // {value: undefined, done: true}
在执行 4 次 next
方法后,生成器对象的状态为 closed
,缺少任何一次 next
,生成器对象的状态都会为 suspended
:
这说明生成器函数的 yield
关键字可以分割函数体内部的代码,并且 yield
可以作为 next
中的 value
值
3.1 生成器函数的 next 方法
next
方法对于生成器函数有很重大的意义
function* generatorFn() {
console.log(1);
}
const generator = generatorFn(); // 不输出任何代码
调用生成器工厂函数的时候,并不会执行生成器函数内部的代码,只有执行 next
的方法之后,整个工厂函数内部的代码才会执行,一直执行到遇到第一个 yield
之前,就像下面这个例子
function* generatorFn() {
console.log(1);
yield 1;
console.log(2);
yield 2;
}
const generator = generatorFn();
console.log(generator.next());
调用一次 next
方法,代码只会运行到一个 yield
之前停止位置
我们可以认为
next
方法和yield
关键字控制整个生成器工厂函数内部的代码运行和暂停的过程
这也是生成器最重大的意义
3.2 生成器函数的 return 值
return
的行为和 yield
十分类似
function* generatorFn() {
return 1;
}
const generator = generatorFn();
generator.next(); // {value: 1, done: true}
generator; // closed 状态
它们的不同之处:通过 yield
关键字退出的生成器函数会处在 done: false
状态;通过 return
关键字退出的生成器函数会处于 done: true
状态。
对于没有明确 return
值的函数,默认 return undefined
,所以在执行了超出 yield
关键字个数 next
方法之后,也会让生成器对象的状态变为 closed
3.3 yield 输入与输出
输入:
function* generatorFn(initVal: number) {
console.log(initVal);
console.log((yield) as number);
console.log((yield) as number);
}
const generator = generatorFn(1); // 传给 initVal
generator.next(2); // 不会输出
generator.next(3); // 3
generator.next(4); // 4
生成器工厂函数
和 next
都可以传参,第一次调用 next
传入的值不会被使用,因为这一次调用是为了开始执行生成器函数
输出就比较好理解了:
下面这个例子利用生成器模拟 Array.prototype.fill
function initArray(n: number, x: any) {
return Array.from(
(function* (n: number, x: any) {
while (n--) {
yield x;
}
})(n, x)
);
}
initArray(5, 0); // [0, 0, 0, 0, 0]
3.4 yield*
function* generatorFn() {
yield 1;
yield 2;
yield 3;
}
对于 yield
输出的情况,如果想同时大批量输出,可以使用 yield*
,该操作符接收一个可迭代对象
function* generatorFn() {
yield* [1, 2, 3];
}
与下面的代码等效
function* generatorFn() {
for (const i of [1, 2, 3]) {
yield i;
}
}
使用生成器可以很好地简化迭代器
const obj = {
value: '123',
*[Symbol.iterator]() {
yield* this.value;
},
};
for (const i of obj) {
console.log(i);
}
// 1
// 2
// 3
4. 停止生成器
生成器对象除了多次执行 next
方法,消耗尽 yield
之外停止生成器,还有 return
和 throw
方法可以更加主动的停止生成器,这两个方法在任意的生成器对象中都有,不同于自定义迭代器需要手动添加
4.1 return
return
方法会强制生成器进入关闭状态
function* generatorFn(): Generator<number, void | string, undefined> {
yield* [1, 2, 3];
}
const g = generatorFn();
console.log(g.next());
console.log(g.return('结束'));
console.log(g.next());
生成器只要进入 closed
状态,就无法恢复了
4.2 throw
throw
方法会在暂停的时候将一个提供的错误注入到生成器对象中。如果错误未被处理,生成器就会关闭
function* generatorFn(): Generator<number, void | string, undefined> {
yield* [1, 2, 3];
}
const g = generatorFn();
try {
g.throw('错误');
} catch (e) {
console.log(e); // 错误
}
console.log(g.next()); // {value: undefined, done: true}
在外部处理错误,生成器对象状态依然会被关闭
如果在内部处理错误,生成器就不会被关闭:
function* generatorFn(): Generator<number, void | string, undefined> {
for (const i of [1, 2, 3]) {
try {
yield i;
} catch (e) {
console.log(e);
}
}
}
const g = generatorFn();
g.next();
g.throw('结束');
console.log(g.next()); // {value: 3, done: false}
5. 生成器与 Promise 控制异步流程
我们我知道了 yield
可以配合 next
方法一起控制整个生成器函数的暂停与恢复执行的流程,所以,生成器也可以控制异步流程
7张图,20分钟就能搞定的async/await原理!为什么要拖那么久? - 掘金 (juejin.cn)
,这篇文章详细讲解了 async ~ await
语法糖
这里奉上 ts
的写法:
function fn(nums: number): Promise<number> {
return new Promise(resolve => {
setTimeout(() => {
resolve(nums * 2);
}, 1000);
});
}
function* gen(): Generator<any, Promise<number>, any> {
const num1 = yield fn(1);
const num2 = yield fn(num1);
const num3 = yield fn(num2);
return num3;
}
// async 语法糖
function generatorToAsync(
generatorFn: (...args: any[]) => Generator<any, Promise<any> | string, any>
) {
return function (this: any, ...args: any[]) {
const g = generatorFn.apply(this, args);
return new Promise((resolve, reject) => {
function go(key: 'throw' | 'next', arg?: any): Promise<any> | void {
let res: IteratorResult<any, string | Promise<any>>;
try {
res = g[key](arg);
} catch (err) {
return reject(err);
}
const { value, done } = res;
if (done) {
return resolve(value);
} else {
return Promise.resolve(value).then(
(val: any) => go('next', val),
(err: any) => go('throw', err)
);
}
}
go('next');
});
};
}
// 使用
// generatorToAsync 包装普通函数就可以变成 async 函数
const asyncFn = generatorToAsync(gen);
const res = asyncFn();
res.then(value => {
console.log(value);
});
总结
- 生成器是一种特殊的函数。在关键字
function
后加上一个*
就可以将这个函数声明为生成器函数。 - 调用生成器函数,返回一个内置了
Symbol.iterator
可迭代协议的生成器对象 - 我们可以使用
yield
和next
控制生成器函数的执行过程