Generator (协程机制)
Generator(生成器)提供了一种可暂停和恢复控制任务执行的方式。通过 yield 关键字交出执行权,通过 next() 方法恢复执行并传入值。
面试考点:虽然在实际业务代码中我们很少直接写 Generator,但它是
async/await的底层基础,同时也是前端处理复杂异步流(如 Redux-Saga)的核心。
function* getNum() {
console.log("开始执行");
const a = yield 1;
console.log("传入的 a:", a);
const b = yield 2;
console.log("传入的 b:", b);
return 3;
}
const gen = getNum(); // 调用生成器函数,并不执行内部代码,而是返回一个迭代器
console.log(gen.next().value); // 打印 '开始执行',并遇到 yield 1 暂停,返回 1
console.log(gen.next("我是a").value); // 恢复执行,将 '我是a' 赋值给变量 a,遇到 yield 2 暂停,返回 2
console.log(gen.next("我是b").value); // 恢复执行,将 '我是b' 赋值给变量 b,遇到 return 3 结束,返回 3
底层实现 (Babel 转译)
Generator 是如何实现“函数暂停”的呢?其实底层的本质是一个状态机(State Machine)和闭包上下文保存。
通过 Babel 转译后的核心骨架大致如下:
// 模拟 Babel 转译后的结构
function getNum() {
let state = 0; // 状态指针
let context = {}; // 用于保存上下文变量(比如上文的 a, b)
// 内部包裹一个 switch-case 状态机
return {
next: function (val) {
while (1) {
switch (state) {
case 0:
console.log("开始执行");
state = 1; // 更新状态指针,指向下一个切点
return { value: 1, done: false };
case 1:
context.a = val; // 接收 next 传进来的值
console.log("传入的 a:", context.a);
state = 2;
return { value: 2, done: false };
case 2:
context.b = val;
console.log("传入的 b:", context.b);
state = 3;
return { value: 3, done: true };
}
}
},
};
}
总结:每一次执行到 yield,其实都执行了一遍内部被包裹的函数。只是在这个过程中,利用了一个 state 变量来控制 switch-case 走到哪个分支,并用了一个 context 对象储存上下文。这使得每次调用 next() 时,都可以无缝接着上一次的代码位置继续执行。
实际开发中的应用场景
在 async/await 普及后,我们确实很少在业务代码中直接手写 Generator 来处理异步了。但在一些底层框架设计和特定的复杂场景下,它仍然是无可替代的:
1. 复杂异步状态管理(如 Redux-Saga)
这是目前 Generator 最著名的应用场景。在处理诸如“防抖”、“节流”、“任务取消(Cancel)”、“任务竞态(Race)”等复杂的异步流时,async/await 会显得力不从心(因为 Promise 一旦发出就无法取消)。
由于 Generator 可以主动交出控制权(yield),框架可以在外部捕获这个信号,并在需要的时候决定是否还要调用 next() 恢复执行,从而完美实现异步任务的中断与取消。
2. 部署自定义的迭代器 (Iterable)
当我们需要遍历一个非常规的数据结构(比如一棵树、一个图,或者自己封装的链表),我们可以为该对象部署 Symbol.iterator 接口,用 Generator 来实现极其优雅的遍历:
const customObj = {
data: [1, 2, 3],
*[Symbol.iterator]() {
for (let item of this.data) {
yield item * 2; // 自定义迭代逻辑
}
},
};
// 外部可以直接使用 for...of 进行遍历
for (const val of customObj) {
console.log(val); // 2, 4, 6
}
3. 处理海量数据 / 无限序列(惰性求值)
如果我们需要生成一个巨大的序列(比如一百万个 ID,或者斐波那契数列),直接把所有数据放进数组里会瞬间撑爆内存(O(n) 空间复杂度)。
使用 Generator 则是惰性求值(Lazy Evaluation)的,只有在调用 next() 时才会去计算下一个值,内存占用永远是 O(1)。
// 生成无限的递增 ID
function* generateId() {
let id = 1;
while (true) {
yield id++;
}
}
const idMaker = generateId();
console.log(idMaker.next().value); // 1
console.log(idMaker.next().value); // 2
// 永远不会内存溢出,按需索取