理解 JavaScript 的迭代器与生成器
导航
你的目的 | 推荐阅读路径 |
---|---|
快速入门 理解基本概念 | → Part 1: 迭代器基础 → Part 2.1-2.2: 生成器语法 → Part 5.2: 何时使用生成器 |
实战应用 解决实际问题 | → Part 5.2: 何时使用生成器(决策树) → Part 4: 生成器的三种应用 → Part 5.3: 常见陷阱 → Part 2.3: 生成器的控制流 |
性能优化 提升代码效率 | → Part 4.3: 性能优化工具 (惰性求值视角) → Part 4.2: Iterator Helpers API → Part 5.3: 常见陷阱 |
调试问题 解决具体 bug | → Part 5.3: 常见陷阱(直接看) → Part 1.4: 可迭代对象, 迭代器, 迭代结果的关系 → Part 2.2: 不可回退性 |
深入原理 理解底层机制 | → Part 1-2: 基础(快速复习) → Part 3: 两种实现路径 👹 |
Part 1: 迭代器基础
1.1 什么是迭代器
迭代器是一种设计模式, 它允许使用者逐个遍历一个对象中的元素, 但无需关心该对象的内部实现.
在迭代器出现前, 不同数据结构需要使用不同的遍历方式
const usersArray = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' }
];
const usersMap = new Map([
['user5', { id: 5, name: 'Eve' }],
['user6', { id: 6, name: 'Frank' }]
]);
const usersSet = new Set([
{ id: 7, name: 'Grace' },
{ id: 8, name: 'Henry' }
]);
// 需要针对每种数据结构写不同的遍历逻辑
function processUsersOldWay(dataSource, type) {
if (type === 'array') {
for (let i = 0; i < dataSource.length; i++) {
console.log(dataSource[i].name);
}
} else if (type === 'map') {
dataSource.forEach((user, key) => {
console.log(user.name);
});
} else if (type === 'set') {
dataSource.forEach(user => {
console.log(user.name);
});
} else {
throw new RangeError("type out of range");
}
}
processUsersOldWay(usersArray, 'array');
processUsersOldWay(usersMap, 'map');
processUsersOldWay(usersSet, 'set');
有了迭代器之后, 获取对象内部元素的工作由对象内部实现, 外部无需关心数据结构类型
function processUsersNewWay(dataSource) {
for (let user of dataSource.values()) {
console.log(user.name);
}
}
processUsersNewWay(usersArray);
processUsersNewWay(usersMap);
processUsersNewWay(usersSet);
1.2 JavaScript 迭代器协议
JavaScript 要求迭代器对象必须有一个 next()
方法, 每次调用返回迭代结果:
{
value: any, // 当前值
done: boolean // 是否迭代完成
}
举例: 手写一个 Range
迭代器
function createRangeIterator(start, end) {
let current = start;
return {
next() {
if (current <= end) {
return { value: current++, done: false };
}
return { done: true };
}
};
}
const iter = createRangeIterator(0, 2); // iter 是一个产生 0, 1, 2 序列的迭代器
console.log(iter.next()); // { value: 0, done: false }
console.log(iter.next()); // { value: 1, done: false }
console.log(iter.next()); // { value: 2, done: false }
console.log(iter.next()); // { done: true }
1.3 可迭代对象
可迭代对象是实现了 Symbol.iterator
方法的对象. 调用对象的 Symbol.iterator
应该返回一个迭代器
const iterableObject = {
[Symbol.iterator]() {
let count = 0;
return {
next() {
if (count < 3) {
return { value: count++, done: false };
}
return { done: true };
}
};
}
};
可迭代对象可以被 for...of
, ...
逐项消费
console.log(...iterableObject); // 0 1 2
for(const item of iterableObject){
console.log(item); // 0 1 2
}
1.4 可迭代对象, 迭代器, 迭代结果的关系
理清概念
这三个概念很容易混淆, 可迭代对象定义了如何获取迭代器, 迭代器定义了如何获取数据, 迭代结果定义了本次访问到的数据.
举个例子
// 1. 可迭代对象: 实现了返回迭代器方法的对象
// 可迭代对象可以被 for...of, ... 运算符消费
const iterableObject = {
[Symbol.iterator]() {
let count = 0;
return {
next() {
if (count < 3) {
return { value: count++, done: false };
}
return { done: true };
}
};
}
};
// 2. 迭代器: 拥有 next 方法的对象
// 迭代器只能通过 `.next` 消费获取元素
const iter = iterableObject[Symbol.iterator]();
console.log(iter); // {next: ƒ}
// 3. 迭代结果: 调用迭代器 next 得到的对象
const result = iter.next();
console.log(result); // {value: 0, done: false}
可迭代迭代器
迭代器定义了如何获取数据, 但是 JavaScript 语法对迭代器并不友好, 用户只能通过 .next
访问数据, 不能通过 for...of
, ...
等语法便捷的使用迭代器. 针对这种问题, 用户可以为迭代器实现 [Symbol.iterator]()
方法让迭代器可以像可迭代对象一个易于使用. 这种迭代器被称为 可迭代迭代器.
事实上很多内置类型的迭代器都是可迭代对象. JavaScript 也期望所有迭代器都应该是可迭代的.
// arr 是一个可迭代对象
const arr = [1,2,3,4];
// 获取数组的迭代器
const arr_iter = arr[Symbol.iterator](); // Array Iterator {}
// 数组的迭代器具有 Symbol.iterator 因此数组的迭代器是一个 可迭代迭代器
arr_iter[Symbol.iterator]; // ƒ [Symbol.iterator]() { [native code] }
// 尝试获取可迭代迭代器的迭代器
const arr_iter2 = arr_iter[Symbol.iterator](); // Array Iterator {}
// 因此可以展开一个迭代器
console.log(...arr); // 1 2 3 4
console.log(...arr_iter); // 1 2 3 4
MDN
很容易使一个迭代器也可迭代: 只需实现
[Symbol.iterator]()
方法, 并返回它的this
.// Satisfies both the Iterator Protocol and Iterable const myIterator = { next() { // ... }, [Symbol.iterator]() { return this; }, };
这种对象被称为可迭代迭代器. 这样做可以让期望可迭代对象的各类语法使用此类迭代器 - 因此, 在不实现可迭代协议的情况下, 仅实现迭代器协议的作用很小. (事实上, 几乎所有的语法和 API 都期待可迭代对象, 而不是迭代器) 生成器对象是一个例子:
这种设计确实让迭代器更易于消费了, 但是也让人很容易搞混 迭代器 和 可迭代对象
提问: 可迭代对象 [Symbol.iterator]
方法在多次调用中是否应该返回相同的迭代器呢?
取决于你的使用场景:
每次返回新的独立迭代器
此类可迭代对象可以被多次遍历, 多个迭代器互不干扰. 大多数 JavaScript 内置可迭代对象 (
Array
,Map
...) 都是用此模式const iterableObject = { [Symbol.iterator]() { let count = 0; // 每次调用都创建新的 count return { next() { if (count < 3) { return { value: count++, done: false }; } return { done: true }; } }; } }; // 每次调用返回不同的迭代器 const iter1 = iterableObject[Symbol.iterator](); const iter2 = iterableObject[Symbol.iterator](); console.log(iter1 === iter2); // false - 不同的迭代器对象 console.log(iter1.next()); // { value: 0, done: false } console.log(iter1.next()); // { value: 1, done: false } console.log(iter2.next()); // { value: 0, done: false } - 不受前次调用的影响 console.log(iter2.next()); // { value: 1, done: false } // 可以多次遍历, 每次都是独立的 for (let x of iterableObject) { console.log(x); // 0, 1, 2 } for (let x of iterableObject) { console.log(x); // 0, 1, 2 - 重新开始 }
返回同一个迭代器
已经迭代过的数据无法再次被迭代. JS 中的
String.prototype.matchAll()
就采用了此模式. 这样设计一般是因为它们的内部状态是 流模式处理, 单向前进, 不可重置的⚠️ 注意: 这种 "一次性消费" 的设计在实际开发中较少使用, 因为它违反了大多数开发者的预期(通常期望可迭代对象可以被多次遍历). 这里仅作为演示
Symbol.iterator
返回同一迭代器的可行性.// 一次性消费的任务队列 function createMessageQueue(messages) { let index = 0; const queueIterator = { next() { if (index < messages.length) { return { value: messages[index++], done: false }; } return { done: true }; }, }; return { [Symbol.iterator]() { return queueIterator; } }; } const queue = createMessageQueue(["1. 洗衣服", "2. 做饭", "3. 刷碗", "4. 擦地", "5. 擦玻璃"]); const iter1 = queue[Symbol.iterator](); const iter2 = queue[Symbol.iterator](); console.log(iter1 === iter2); // true console.log(iter1.next()); // {value: '1. 洗衣服', done: false} console.log(iter2.next()); // {value: '2. 做饭', done: false} for (let msg of queue) { console.log(msg); // '3. 刷碗' break; } console.log([...queue]); // ['4. 擦地', '5. 擦玻璃'] console.log([...queue]); // []
1.5 JavaScript 中的内置可迭代对象
很多内置对象都是可迭代对象: Array
, String
, Map
, Set
. 这些内置对象一般通过 for...of
, ...
消费
const arr = [1, 2, 3];
for (const item of arr) {
console.log(item);
}
// 等价于:
const iterator = arr[Symbol.iterator]();
let result = iterator.next();
while (!result.done) {
const item = result.value;
console.log(item);
result = iterator.next();
}
const arr = [1, 2, 3];
const newArr = [...arr];
// 展开运算符等价于:
const iterator = arr[Symbol.iterator]();
let result = iterator.next();
while (!result.done) {
newArr.push(result.value);
result = iterator.next();
}
console.log(newArr); // [1, 2, 3]
Part 2: 生成器基础
2.1 生成器基础语法
背景与动机
手动实现迭代器的 next
方法很痛苦, 开发者需要手动维护迭代状态并构造返回对象, 这不仅开发成本高, 代码也缺乏可读性
// 手写迭代器: 需要手动管理状态变量
function createRangeIterable(start, end) {
return {
[Symbol.iterator]() {
let current = start; // 😓 需要手动维护状态
return {
next() {
if (current <= end) {
return { value: current++, done: false };
}
return { done: true };
}
};
}
};
}
基本语法
生成器通过 function*
和 yield
关键字, 让开发者用顺序, 直观的方式编写迭代器逻辑:
定义生成器: JavaScript 使用
function*
语法声明生成器函数.首次执行: 外部调用生成器函数不会立即执行函数体, 而是返回一个迭代器对象.
逐步执行
- 外部调用迭代器的
next()
方法时, 生成器函数体开始执行 - 遇到
yield
表达式时暂停, 并返回yield
后面的值 - 再次调用
next()
时, 从上次暂停的位置继续执行 - 直到函数结束或遇到
return
语句
- 外部调用迭代器的
// 🎉 生成器版本: 简洁明了
function* createRange(start, end) {
for (let i = start; i <= end; i++) {
yield i; // 自动暂停, 返回值
}
}
委托生成: yield*
生成器还支持使用 yield*
将 "yield
迭代生成一个个元素的过程" 从函数体中委托出去.
// 二叉树前序遍历的例子
function* traverseTree(node) {
if (!node) return;
yield node.value; // 当前节点
yield* traverseTree(node.left); // 左子树
yield* traverseTree(node.right); // 右子树
}
工作原理:
yield*
后面接收一个可迭代对象 (如数组, 生成器等), 当执行到 yield*
时:
- 生成器将控制权完全交出
- 由被委托的迭代器负责产出值
- 直到被委托的迭代器耗尽, 才继续执行后续代码
类似于函数 "将控制权转移" 给其他函数以实现业务逻辑
注意:
yield*
只要求后接一个可迭代对象, 不限制必须接生成器.
尝试在形式上解释 *
符号
我个人的理解, 非官方
function*
看起来像是function
+*
.yield*
看起来是yield
+*
.
为什么 JavaScript 在设计的时候总是通过 加 *
定义一个新语法呢?
我的理解是 *
表示 "打包为一组的可迭代的数据",
function*
表示这个函数和普通函数不一样, 他不是产生 "一个返回值" 的function
, 而是产生 "一组可迭代数据的打包" 的function
.yield*
表示这个yield
和普通yield
不一样, 他不是产出一次迭代数据的yield
, 而是产出 "一组可迭代的多组数据" 的yield
这么理解的话, *
与 ...
似乎是成对存在的:
*
的意思是返回 "打包为一组可迭代数据"...
的意思是将 "将一组可迭代数据展开"
2.2 生成器的不可回退性
为什么生成器是一次性的?
生成器对象内部维护着执行状态 (当前执行到哪个 yield
, 局部变量的值等). 这些状态是 逐步推进, 不可回退 的:
function* gen() {
let count = 0;
yield ++count; // 1
yield ++count; // 2
yield ++count; // 3
}
const g = gen();
console.log(g.next().value); // 1
console.log(g.next().value); // 2
如果允许"重新开始", 要么需要复制整个执行上下文 (成本高昂), 要么就需要重新调用生成器函数创建新对象. 因此生成器天然就是一次性的
基于这个特性, 不难理解为什么生成器返回对象的特殊性质: 生成器返回的对象是可迭代迭代器, 且这个对象的迭代器还是他自己
function* gen() {
yield 1;
yield 2;
yield 3;
}
const g = gen();
console.log(g.next); // ƒ next() { [native code] }
console.log(g[Symbol.iterator]); // ƒ [Symbol.iterator]() { [native code] }
console.log(g[Symbol.iterator]() === g); // true
console.log(g.next().value); // 1
console.log([...g]); // [2, 3]
2.3 生成器的控制流
生成器向外提供双向数据交换, 终止执行, 错误处理能力
next(value)
- 传值给生成器, 允许生成器通过外部输入决定迭代逻辑通过
next(value)
传入的值可以被input = yield output
的 input 收到// gen(x) -> [x * 2, x + y] function* gen() { console.log('INIT'); const x = yield 'x = '; console.log('RECEIVED: x =', x); const y = yield x * 2; console.log('RECEIVED: y =', y); return x + y; } const g = gen(); console.log(g.next()); // {value: 'x = ', done: false} console.log(g.next(10)); // RECEIVED: x = 10, {value: 20, done: false} console.log(g.next(5)); // RECEIVED: y = 5, {value: 15, done: true}
return(value)
- 允许调用方提前结束生成器调用
return
相当于在生成器内部执行return
, 仍然会运行finally
等逻辑// 不断从数据源接受消息 function* genMessage() { const ws = new WebSocket("wss://echo.websocket.org"); try { while (true) { yield new Promise((resolve) => { ws.onmessage = (event) => resolve(event.data); }); } } finally { // 执行 取消执行, 清理资源 操作 ws.close(); } } // 处理生成器生成的消息, 直到听到 "OVER" async function processMessages() { const messageGenerator = genMessage(); for (const messagePromise of messageGenerator) { const message = await messagePromise; console.log("Received message:", message); if (message === "OVER") { messageGenerator.return(); } } } processMessages();
throw(error)
- 向生成器传入异常, 允许生成器感知并处理外部异常注意:
throw
也有返回值, 其返回值是下一个yield
产生的值. 即 throw 不仅传入了一个错误, 造成生成器内部抛出错误, 还会推进生成器继续执行. 可以将throw
理解为一个传入错误的next
.function promoCodeValidate(input) { if (typeof input !== "string") { throw new TypeError("Promo code must be a string."); } if (input !== "WELCOME10") { throw new Error("Invalid promo code."); } return true; } // 如果外部反馈类型错误, 就换下一种类型的促销码 // 如果外部反馈其他错误, 就尝试同类型下一个促销码 function* promoCodeGenerator() { let numberCode = [100, 200]; let stringCode = ["SUMMER20", "WELCOME10", "FALL30"]; let codes = [numberCode, stringCode]; for (let codeGroup of codes) { for (let code of codeGroup) { try { yield code; } catch (error) { if (error instanceof TypeError) { break; } } } } } // 尝试找出所有可用的促销码 function tryToRedeemAllPromoCode() { const generator = promoCodeGenerator(); let code = generator.next().value; while (code !== undefined) { console.log(`- Trying to redeem promo code: ${code}`); try { promoCodeValidate(code); console.log(`✅ Promo code "${code}" redeemed successfully!`); code = generator.next().value; } catch (error) { code = generator.throw(error).value; } } } tryToRedeemAllPromoCode(); // - Trying to redeem promo code: 100 // - Trying to redeem promo code: SUMMER20 // - Trying to redeem promo code: WELCOME10 // ✅ Promo code "WELCOME10" redeemed successfully! // - Trying to redeem promo code: FALL30
用 return()
和 throw()
的场景不多?
是的, 大多数简单的顺序迭代场景只需要 next()
即可实现, JavaScript 中消费迭代器常见的 for...of
, ...
等便利语法也只支持 next()
. 这是因为它们专为简单的顺序迭代而设计. 在这类场景下, 灵活的生成器退化为普通的迭代器, return()
和 throw()
确实用不上.
但当需要以下能力时, 手动调用这三个方法是必需的:
资源管理 - 使用
return()
触发finally
清理资源 (如上面的 WebSocket 例子)补充: 如果仅使用
for...of
语法消费生成器, 可以使用break
实现类似return
的效果提前终止 - 根据外部条件优雅地结束迭代
错误协商 - 使用
throw()
让生成器感知外部错误并调整策略 (如上面的促销码例子)
这三个方法构成了生成器作为协程的完整控制协议. 它们的使用频率取决于场景复杂度: 简单迭代只需 next()
, 复杂控制流则三者缺一不可.
2.4 异步生成器
语法
- 使用
async function*
定义生成器, 允许在生成器内部使用await
- 生成器返回的迭代器为 异步迭代器 (实现了
Symbol.asyncIterator
), 迭代器next
返回Promise<{value, done}>
- 使用
for await...of
异步遍历数据
async function* asyncGenerator() {
yield await Promise.resolve(1);
yield await Promise.resolve(2);
yield await Promise.resolve(3);
}
// 使用 for await...of 遍历
for await (const value of asyncGenerator()) {
console.log(value); // 1, 2, 3
}
解决问题
看上面的描述, 异步生成器似乎并没有解决任何新问题, 我大可以说: "我完全可以将所有异步过程通过 yield promise
产出, 让外部 await
执行, 然后通过 .next
方法将结果反馈给迭代器, 实现异步迭代".
是的, 异步生成器并没有突破生成器的能力边界, 其解决的核心问题是迭代逻辑的封装位置. 以一个用户登录流程控制为例
// 使用异步生成器
async function* generatorLogin() {
// yield 出去, 等待外部传入凭据
const [userId, password] = yield 'NEED_CREDENTIALS';
// 调用登录API 获取用户信息
// ✨ 可以在生成器内部await
const userInfo = await LoginAPI.login(userId, password);
// 检查用户类型
if (userInfo.type === 'admin') {
// 管理员需要OTP
const otp = yield 'NEED_OTP';
// ✨ 可以在生成器内部await
const success = await LoginAPI.verifyOTP(otp);
if (success) {
return userInfo;
} else {
throw new Error('OTP验证失败');
}
} else {
return userInfo;
}
}
// 调用者只需要关心业务流程
async function login() {
const gen = generatorLogin();
let result = await gen.next();
result = await gen.next(['admin', '123456']);
if (!result.done && result.value === 'NEED_OTP') {
result = await gen.next('0000');
}
console.log('登录完成:', result.value);
}
// 使用生成器
function* generatorLogin() {
const [userId, password] = yield 'NEED_CREDENTIALS';
// yield一个Promise, 等待外部 await 过程并传回结果
const userInfo = yield LoginAPI.login(userId, password);
if (userInfo.type === 'admin') {
const otp = yield 'NEED_OTP';
// yield一个 Promise, 等待外部 await 过程并传回结果
const success = yield LoginAPI.verifyOTP(otp);
if (success) {
return userInfo;
} else {
throw new Error('OTP验证失败');
}
} else if (userInfo.type === 'guest') {
console.log('[生成器] Guest用户, 直接返回');
return userInfo;
}
}
// 调用者需要手动协调每一步
async function login() {
const gen = generatorLogin();
let result = gen.next();
result = gen.next(['admin', '123456']);
const userInfo = await result.value;
result = gen.next(userInfo);
if (result.value === 'NEED_OTP') {
result = gen.next('0000');
}
if (result.value && typeof result.value.then === 'function') {
const otpSuccess = await result.value;
result = gen.next(otpSuccess);
}
console.log('登录完成:', result.value);
console.log('');
}
通过上述对比, 我们可以看到: 从能力角度看, 异步生成器并非 "绝对必需", 理论上可以通过 next()
传值来手动控制迭代流程. 但从工程角度看, 异步生成器的价值在于:
- 更符合迭代器 "对内封装遍历流程, 对外只暴露迭代能力" 的思想
- 职责分离: 生成器负责数据的产生逻辑和迭代控制, 调用者负责数据的消费逻辑, 调用方无需关心生成器的内在逻辑
总结: 异步生成器让我们专注于数据流本身, 而不是迭代控制的细节
Part 3: 实现生成器的两种路径
生成器是一种语言特性, 不同层次有不同的实现方式: - 转译器层面 (Babel): 用状态机模拟生成器行为 - 引擎层面 (V8/SpiderMonkey): 用非对称协程机制原生实现
ES6 引入生成器时, 许多老运行时还不支持. 因此 Babel 会等工具将生成器转换为状态机模拟的 ES5 兼容代码, 而在现在 JavaScript 运行时中, 运行时是通过实现非对称协程机制直接实现的生成器 ## 3.1 转译器实现 - 状态机模拟
生成器的状态机模型
生成器在运行时有四种状态: 初始态 (suspended-start
), 执行态 (executing
), 暂停态(suspended-yield
), 完成态(completed
). 这是一个典型的有限状态机.
stateDiagram-v2 [*] --> 初始态: 调用生成器函数 初始态 --> 执行态: next() 初始态 --> 完成态: return(v) 跳过所有代码 执行态 --> 暂停态: yield 执行态 --> 完成态: return 或 未捕获异常 暂停态 --> 执行态: next(v) 暂停态 --> 执行态: throw(e) 在 yield 处抛异常 暂停态 --> 完成态: return(v) 提前结束 完成态 --> [*] note right of 初始态 生成器对象已创建 但内部代码尚未开始执行 end note note right of 执行态 正在执行生成器内部代码 end note note right of 暂停态 暂停在某个 yield 等待 next/throw/return 调用 end note note right of 完成态 done: true (不可逆) 后续调用始终返回 done: true end note
Babel 转译示例
function* fib(){
let a=1, b=2;
while(a<Number.MAX_SAFE_INTEGER){
yield a;
[a, b] = [b, a+b];
}
}
const fib_iter = fib(); // 进入初始态
fib_iter.next(); // 进入执行态
fib_iter.return(); // 进入完成态
function _regenerator() { /*! regenerator-runtime ... */ }
function _regeneratorDefine2(e, r, n, t) { /* ... */ }
var _marked = /*#__PURE__*/_regenerator().m(fib);
function fib() {
var a, b, _ref; // 通过闭包实现迭代状态保存
return _regenerator().w(function (_context) {
// _context.n 记录的代码执行的位置, 类似 PC. 注意这不是上图中状态机的状态
// 每次调用 next() 时, 生成器进入"执行态"
while (1) switch (_context.n) {
case 0: // 从初始态切换到执行态的过程
a = 1, b = 2;
case 1: // 从执行态切换到暂停态的过程
if (!(a < Number.MAX_SAFE_INTEGER)) {
_context.n = 3; // 循环结束, 切换到完成态
break;
}
_context.n = 2;
return a;
case 2: // 暂停态恢复到执行态的过程. 这段执行完成后还需要执行 case1 才能到达暂停态
_ref = [b, a + b];
a = _ref[0];
b = _ref[1];
_context.n = 1;
break;
case 3: // 从任意状态切换到完成态
return _context.a(2);
}
}, _marked);
}
var fib_iter = fib(); // 进入初始态
fib_iter.next(); // 进入执行态
fib_iter.return(); // 进入完成态
可以看到, Babel
状态的保存
通过
_context.n
保存状态机执行状态 (0
= 初始态,1...n-1
= 执行态, 每次yield
前后分为两个状态,n
= 清理态逻辑 )通过闭包完成生成器逻辑中的变量保存
暂停与恢复的实现
通过
return
抛出数据由 Babel 包裹为{value: any, done: bool}
数据外部通过反复执行函数实现
next
局限性:
- 并没有实现真正的暂停执行函数, (而是每次都从头执行转移后的函数)
- 无法真正保存调用栈 (只是平铺了代码)
3.2 生成器的实现 - 非对称协程
3.2.1 什么是协程 (Coroutines)
协程是可以暂停与恢复执行的函数
- 普通函数: 外部调用 -> 程序执行 -> 返回结果
- 协程: 外部调用 -> 程序执行 -> 程序主动暂停 -> 外部恢复 -> 程序执行 -> ... -> 最终返回
3.2.2 协程 & 线程 & 进程的区别
三者经常放在一起说, 但实际上是两个层面的东西:
- 进程, 线程: 操作系统层面的概念, 由操作系统完成 调用, 中断, 资源管理
- 协程: 程序内部的概念, 由应用逻辑完成 调用, 中断, 资源
相同点: 三者都是一个可被执行, 可被打断的程序
关系: 进程和线程是多对多的关系. 多个协程可以属于一个线程, 一个线程同时只有一个协程在执行, 协程的调度逻辑是由应用程序协作式声明是实现的. 可以将协程理解为一个小线程, 在线程内部交替执行, 交替逻辑由程序定义
这里有一个更加准确的对比
特性 | 进程 | 线程 | 协程 |
---|---|---|---|
调度者 | 操作系统 | 操作系统 | 程序自己 |
暂停执行 | 可以 | 可以 | 可以 |
恢复执行位置 | 从上次执行处恢复 | 从上次执行处恢复 | 从上次执行处恢复 |
恢复执行时栈 | 保留 | 保留 | 保留 |
调度方式 | 抢占式 (随时打断) | 抢占式 (随时打断) | 协作式 (主动 yield 交出) |
内存 | 独立地址空间 | 共享进程内存 | 共享线程内存 |
切换开销 | 很大 | 较大 | 极小 |
并行 | 可以 (多核) | 可以 (多核) | 不能 (单线程内切换) |
需要锁 | 是 | 是 | 否 |
3.2.3 什么是非对称协程
协程有两种基本形式:
对称协程
任何协程可以直接切换到任何其他协程.
-- Lua 示例 (对称协程) co1 = coroutine.create(function() print("co1") coroutine.yield(co2) -- 直接切换到 co2 end) co2 = coroutine.create(function() print("co2") coroutine.yield(co1) -- 直接切换到 co1 end)
控制流可以任意跳转
需要显式管理协程关系
适合实现复杂的状态机
非对称协程 / 半协程 (Asymmetric Coroutines / Semi-coroutines)
非对称协程只实现了协程的暂停/恢复能力, 但限制了切换目标. 非对称协程只能挂起 (suspend / yield) 到调用者, 不能切换到其他协程. 因此, 在非对称协程中, 控制流呈现调用栈结构
特性 | 生成器 (半协程) | 完全协程 |
---|---|---|
yield目标 | 只能yield给调用者 | 可以yield到任意协程 |
控制流 | 调用栈式 | 可任意跳转 |
典型语言 | JavaScript, Python | Lua, Go |
3.2.4 JavaScript 生成器在表现上是非对称协程
function* task() {
console.log('步骤 1');
yield 1; // ← 暂停, 协作式交出控制权
console.log('步骤 2');
yield 2; // ← 暂停, 协作式交出控制权
console.log('步骤 3');
}
const gen = task();
gen.next(); // 暂停, 交出控制权到 task 直到 task 暂停
gen.next(); // 暂停, 交出控制权到 task 直到 task 暂停
gen.next(); // 暂停, 交出控制权到 task 直到 task 暂停
3.2.4 JavaScript 生成器是通过非对称协程实现的
本节介绍的是 SpiderMonkey 的实现 (基于
7d8518812ea2bee7fdd0fa2da03ab479f0075e01
版本, 即撰写时最新的 Firefox 主线版本)GeneratorObject.h
GeneratorObject.cpp
理解栈帧
在深入生成器挂起和恢复机制之前, 我们需要先理解 栈帧 (Stack Frame) 是什么, 因为生成器的挂起恢复本质上就是保存和恢复栈帧快照
栈帧的作用
栈帧是调用栈中的一个单元, 存储一次函数调用的完整执行状态. 每当函数被调用时, 就会在调用栈上创建一个新的栈帧; 函数返回时, 栈帧被销毁.
function outer() { let x = 1; inner(x); // 创建 inner 的栈帧 } // inner 栈帧销毁 function inner(param) { let y = 2; // 此时栈上有两个栈帧: outer 和 inner }
执行到 inner 内部时的调用栈:
┌─────────────────┐ ← 栈顶 (当前执行) │ inner 的栈帧 │ ├─────────────────┤ │ outer 的栈帧 │ ├─────────────────┤ │ 全局栈帧 │ └─────────────────┘
栈帧的数据结构
在 SpiderMonkey 中, 解释器栈帧的结构如下
class InterpreterFrame { // ===== 元数据区(固定大小)===== JSScript* script_; // 正在执行的脚本 (包含字节码) jsbytecode* pc_; // 程序计数器: 当前执行到哪条指令 JSObject* environmentChain_; // 作用域链: 用于变量查找 InterpreterFrame* prev_; // 指向调用者的栈帧 (形成调用链) uint32_t flags_; // 标志位: 函数类型, 执行状态等 Value* argv_; // 参数数组的指针 // ... 其他字段 // ===== 动态数据区(可变大小)===== // Value slots_[???]; // 参数 + 局部变量 + 临时值 };
对应内存布局
低地址 ┌──────────────────────┐ ← InterpreterFrame 解释器栈帧对象起始 │ ===== 元数据区 ===== │ │ script_ │ (指向正在执行的脚本) │ pc_ │ (当前字节码位置) │ environmentChain_ │ (作用域链) │ prev_ │ (调用者栈帧) │ flags_ │ (状态标志) │ argv_ │ (参数指针) ├──────────────────────┤ ← this + 1 = slots() │ ===== 动态数据区 === │ │ slots[0] 参数1 │ │ slots[1] 参数2 │ │ slots[2] 局部变量1 │ │ slots[3] 局部变量2 │ │ slots[4] 临时值1 │ │ ... │ │ slots[n] 操作数栈顶 │ ← sp (stack pointer) └──────────────────────┘ 高地址
栈帧的关键组成
组成部分 作用 示例 script_ 代码本身 函数的字节码指令 pc_ 执行位置 当前执行到第 15 条指令 environmentChain_ 变量查找 查找 x
时沿作用域链向上找prev_ 调用关系 记录是谁调用了这个函数 slots 运行时数据 let x = 5;
的值存在哪里举例:
function fn(n) { let a = 10; // slots[1] (参数 n 在 slots[0]) let temp = a + n; // slots[2] = slots[1] + slots[0] (临时值) return temp; } fn(10);
元数据区: ├─ script_: fn 函数的字节码 ├─ pc_: 当前执行到 "return temp" 指令 ├─ environmentChain_ 函数的作用域 └─ prev_: 指向调用者的栈帧 动态数据区 (slots): ├─ slots[0] = 10 (参数 n) ├─ slots[1] = 10 (局部变量 a) ├─ slots[2] = 20 (临时值 temp = a + n) └─ ... (其他临时计算结果)
生成器需要保存的内容
普通函数执行完就会销毁栈帧, 但生成器需要暂停后再恢复, 在生成器被暂停时运行时需要执行其他程序, 此时生成器所属的栈帧会被销毁, 因此我们必须保存栈帧的以下关键数据
- PC (程序计数器): 记住暂停在哪里, 恢复时从这里继续
- Slots (动态数据): 记住所有局部变量的值, 恢复时还原状态
- Environment Chain (作用域链): 记住如何查找变量, 保持闭包正确
无需存储的字段有
script_
: 函数的字节码不会改变, 恢复时从genObj->callee()
获取prev_
: 调用链在恢复时重建, 创建新栈帧时自动设置flags_
: 状态标志可以重新计算, 根据生成器状态重新设置即可argv_
: 已经存储在动态区中了
明确销毁范围!
暂停时, 因为调用栈要发生变化, 所以生成器的栈帧会被销毁, 但是生成器对象并不会销毁, 因此, 我们要存储的是生成器栈帧中被销毁后不可恢复的对象, 不需要存储栈帧中可被恢复的信息 (如
script_
可以通过生成器对象上的callee
获得)
SpiderMonkey 维护的数据结构
SpiderMonkey 在生成器内部维护了如下对象
// https://searchfox.org/firefox-main/rev/7d8518812ea2bee7fdd0fa2da03ab479f0075e01/js/src/vm/GeneratorObject.h#39
enum {
CALLEE_SLOT = 0, // 调用者函数
ENV_CHAIN_SLOT, // 作用域链
ARGS_OBJ_SLOT, // 参数对象
STACK_STORAGE_SLOT, // 栈存储
RESUME_INDEX_SLOT, // 恢复索引 (程序计数器)
RESERVED_SLOTS
};
其中 RESUME_INDEX_SLOT
是实现协程的核心, 保存了下次应该从哪里执行 (PC)
The resumeIndex slot is abused for a few purposes. It's undefined if it hasn't been set yet (before the initial yield), and null if the generator is closed. If the generator is running, the resumeIndex is RESUME_INDEX_RUNNING.
If the generator is suspended, it's the resumeIndex (stored as JSOp::InitialYield/JSOp::Yield/JSOp::Await operand) of the yield instruction that suspended the generator. The resumeIndex can be mapped to the bytecode offset (interpreter) or to the native code offset (JIT).
生成器通过 RESUME_INDEX_SLOT
的值区分状态
resumeIndex值 | 状态 | 说明 |
---|---|---|
undefined | 初始态 | yield前, 尚未开始执行 |
0 ~ INT32_MAX-1 | 暂停态 | 保存了恢复 yield 位置 (类似 PC) |
INT32_MAX | 执行态 | 正在运行 |
null | 完成态 | 已关闭 |
挂起过程
挂起时, 运行时需要保存当前函数的执行状态, 这些状态维护
/**
* 生成器挂起操作
* cite: https://searchfox.org/firefox-main/rev/7d8518812ea2bee7fdd0fa2da03ab479f0075e01/js/src/vm/GeneratorObject.cpp#132
*
* 核心任务: 将当前栈帧的状态保存到 GeneratorObject, 以便后续恢复执行
*
* @param cx JavaScript 上下文 (用于内存分配, 错误处理等)
* @param obj GeneratorObject (保存挂起状态的容器)
* @param frame 当前正在执行的栈帧 (包含元数据和动态数据)
* @param pc 程序计数器: 当前字节码位置 (yield 指令的位置)
* @param nvalues 栈帧动态数据区元素数 (参数 + 局部变量 + 临时值的总数)
*/
bool AbstractGeneratorObject::suspend(JSContext* cx, HandleObject obj,
AbstractFramePtr frame,
const jsbytecode* pc, // 当前位置
unsigned nvalues // 栈帧上元素数
) {
// 1. 校验挂起时状态是否合法
// 执行生成器方法时 / yield 时 / await 时
MOZ_ASSERT(JSOp(*pc) == JSOp::InitialYield || JSOp(*pc) == JSOp::Yield ||
JSOp(*pc) == JSOp::Await);
auto genObj = obj.as<AbstractGeneratorObject>();
MOZ_ASSERT(!genObj->hasStackStorage() || genObj->isStackStorageEmpty());
MOZ_ASSERT_IF(JSOp(*pc) == JSOp::Await, genObj->callee().isAsync()); // 如果是 await 暂停, 必须在 async 中
MOZ_ASSERT_IF(JSOp(*pc) == JSOp::Yield, genObj->callee().isGenerator()); // 如果是 yield 暂停, 必须在生成器中
// 拷贝动态空间内容
if (nvalues > 0) {
ArrayObject* stack = nullptr;
MOZ_ASSERT(genObj->hasStackStorage());
stack = &genObj->stackStorage();
MOZ_ASSERT(stack->getDenseCapacity() >= nvalues);
if (!frame.saveGeneratorSlots(cx, nvalues, stack)) {
return false;
}
}
// 拷贝 PC
genObj->setResumeIndex(pc);
// 拷贝作用域链
genObj->setEnvironmentChain(*frame.environmentChain());
return true;
}
运行状态 (
PC
) 的保存PC
信息通过yield
索引保存在RESUME_INDEX_SLOT
上.在生成器中, 每个
yield
指令的操作数是一个唯一的索引, 进入暂停态时, 运行时通过PC
求得yield
操作数并存储在RESUME_INDEX_SLOT
上. 在恢复时, 程序通过resumeOffsets()[resumeIndex]
将resumeIndex
映射回字节码 / JIT 代码偏移量, 程序直接用这个偏移量设置PC
, 就能跳到正确位置.为什么要保存
yield
操作数而不是直接保存PC
呢 因为字节码可能会在不同版本之间变化, 使用索引而不是直接的字节码指针更安全// https://searchfox.org/firefox-main/rev/7d8518812ea2bee7fdd0fa2da03ab479f0075e01/js/src/vm/GeneratorObject.cpp#153 genObj->setResumeIndex(pc);
// https://searchfox.org/firefox-main/rev/7d8518812ea2bee7fdd0fa2da03ab479f0075e01/js/src/vm/GeneratorObject.h#144 void setResumeIndex(const jsbytecode* pc) { uint32_t resumeIndex = GET_UINT24(pc); // 从 pc 获取 yield 索引 setFixedSlot(RESUME_INDEX_SLOT, Int32Value(resumeIndex)); // 设置到 RESUME_INDEX_SLOT 上 }
拷贝动态空间内容
动态空间的内容将被拷贝为一个
ArrayObject
, 并存储到STACK_STORAGE_SLOT
上// https://searchfox.org/firefox-main/rev/7d8518812ea2bee7fdd0fa2da03ab479f0075e01/js/src/vm/GeneratorObject.cpp#143 if (nvalues > 0) { // 如果动态空间元素大于0 ArrayObject* stack = nullptr; MOZ_ASSERT(genObj->hasStackStorage()); stack = &genObj->stackStorage(); MOZ_ASSERT(stack->getDenseCapacity() >= nvalues); if (!frame.saveGeneratorSlots(cx, nvalues, stack)) { // 保存栈上动态区 return false; } }
// https://searchfox.org/firefox-main/rev/7d8518812ea2bee7fdd0fa2da03ab479f0075e01/js/src/vm/Stack-inl.h#194 inline bool InterpreterFrame::saveGeneratorSlots(JSContext* cx, unsigned nslots, ArrayObject* dest) const { // 已知动态数据区的元素个数是 nslots, 因此动态数据区的范围就是首元素指针 slots() 到 slots() + nslots return dest->initDenseElementsFromRange(cx, slots(), slots() + nslots); // 将动态区内存拷贝到 dest 数组中 }
// https://searchfox.org/firefox-main/rev/7d8518812ea2bee7fdd0fa2da03ab479f0075e01/js/src/vm/Stack.h#339 // 找到动态数据区的首指针 (指针类型为 Value). 后面会单独解释这是如何实现的 Value* slots() const { return (Value*)(this + 1); }
// https://searchfox.org/firefox-main/rev/7d8518812ea2bee7fdd0fa2da03ab479f0075e01/js/src/vm/NativeObject-inl.h#205 inline bool NativeObject::initDenseElementsFromRange(JSContext* cx, Iter begin, Iter end) { // 一些 asset // 一些容量检查与内存分配 HeapSlot* sp = elements_; size_t slot = 0; for (; begin != end; sp++, begin++) { Value v = *begin; // 获取数据 sp->init(this, HeapSlot::Element, slot++, v); // init 会将 v 赋值给 element[solt++] 同时检查 v 是否是 GC 对象, 如果是则通知 GC } // 后校验流程 return true; }
这里有两个很迷惑的点:
- 为什么
(Value*)(this + 1)
会指向栈帧上动态范围区的起始指针? - 为什么动态范围区是一片连续的内存?
这是利用 C++ 的 Flexible Array Member 技巧 (俗称 struct hack). 在一次内存分配中, C++ 可以同时分配一款连续的内存存储固定大小的元数据和可变长度的数据区:
// InterpreterFrame 的内存布局 class InterpreterFrame { // ===== 固定大小的元数据 ===== JSScript* script_; // 脚本指针 jsbytecode* pc_; // 程序计数器 (返回位置) JSObject* environmentChain_; // 作用域链 InterpreterFrame* prev_; // 调用者栈帧 uint32_t flags_; // 标志位 // ===== 动态大小数据区 (紧跟其后) ===== // Value slots_[???]; // 运行时确定大小 Value* slots() const { // this 是 InterpreterFrame 类型数据 // this + 1 = this 的指针位置 + sizeof(InterpreterFrame) // = this 的指针位置 + 固定区的尺寸 // = 动态区的开始地址 // reinterpret_cast<Value*>(this + 1) 将地址转为 Value 类型指针 // 此后 // reinterpret_cast<Value*>(this + 1) + nslot = 动态区开始位置 + nslot * sizeof(value) return reinterpret_cast<Value*>(this + 1); } };
- 为什么
拷贝作用域链
解引用
environmentChain
保存到ENV_CHAIN_SLOT
上// https://searchfox.org/firefox-main/rev/7d8518812ea2bee7fdd0fa2da03ab479f0075e01/js/src/vm/GeneratorObject.cpp#154 genObj->setEnvironmentChain(*frame.environmentChain()); // 返回 envChain JsObject
// https://searchfox.org/firefox-main/rev/7d8518812ea2bee7fdd0fa2da03ab479f0075e01/js/src/vm/GeneratorObject.h#86 void setEnvironmentChain(JSObject& envChain) { setFixedSlot(ENV_CHAIN_SLOT, ObjectValue(envChain)); }
恢复过程
恢复过程不仅要还原上面存储的内容, 还要重建出元数据区没有存储的内容
/**
* Generator/Async 函数的恢复操作
* cite: https://searchfox.org/firefox-main/rev/7d8518812ea2bee7fdd0fa2da03ab479f0075e01/js/src/vm/GeneratorObject.cpp#267
*
* 核心任务: 从 GeneratorObject 中读取保存的状态, 重建栈帧, 继续执行
*
* @param cx JavaScript 上下文
* @param activation 解释器激活对象 (管理当前执行的栈帧和寄存器)
* @param genObj 要恢复的 GeneratorObject (包含所有保存的状态)
* @param arg 传递给 generator 的参数 (next(arg) 中的 arg)
* @param resumeKind 恢复类型 (normal/throw/return, 对应 next/throw/return)
*/
bool AbstractGeneratorObject::resume(JSContext* cx,
InterpreterActivation& activation,
Handle<AbstractGeneratorObject*> genObj,
HandleValue arg, HandleValue resumeKind) {
// 确保 generator 处于挂起状态 (即 RESUME_INDEX_SLOT 存在 且 RESUME_INDEX_SLOT < int32)
MOZ_ASSERT(genObj->isSuspended());
// ========== 准备恢复所需数据并保护起来, 防止被 GC ==========
// 获取 generator 函数本身 (包含 script, 字节码等信息)
RootedFunction callee(cx, &genObj->callee());
// 获取 ENV_CHAIN_SLOT 上的数据并保护起来 (实现见下)
RootedObject envChain(cx, &genObj->environmentChain());
// ========== 创建新的栈帧 ==========
// 在调用栈上为 generator 创建一个新的 栈帧 (InterpreterFrame)
// 参数:
// callee: generator 函数 (包含要执行的 script)
// envChain: 作用域链 (保证变量查找正确)
// 这会分配一个新的栈帧:
// 空间是 固定元数据区的尺寸 + 动态区 sizeof(Value) * 动态区元素数
// size = sizeof(InterpreterFrame) + nslots * sizeof(Value)
// 并初始化元数据区 (script, environmentChain, prev 等)
// 实现见下
if (!activation.resumeGeneratorFrame(callee, envChain)) {
return false; // 栈帧创建失败 (可能是栈溢出)
}
// ========== 将标记栈帧为"恢复的 generator" ==========
// 设置 `flags_` 标志位, 表示这是一个恢复的 generator 栈帧
activation.regs().fp()->setResumedGenerator();
// ========== 恢复 arguments 对象 ==========
// 如果 generator 函数使用了 arguments 对象
if (genObj->hasArgsObj()) {
// 将保存的 arguments 对象赋值到 argsObj_ 上
activation.regs().fp()->initArgsObj(genObj->argsObj());
}
// ========== 恢复动态数据区==========
// 检查暂停时是否保存了动态区
if (genObj->hasStackStorage() && !genObj->isStackStorageEmpty()) {
// 获取当前栈帧对应的脚本
JSScript* script = activation.regs().fp()->script();
// 获取保存 slots 的数组对象 (在暂停时存储在数组中的)
ArrayObject* storage = &genObj->stackStorage();
// 获取保存的元素数量 (即暂停时的 nvalues)
uint32_t len = storage->getDenseInitializedLength();
// 将保存的 slots 复制回新栈帧的动态数据区 (见下)
activation.regs().fp()->restoreGeneratorSlots(storage);
// 调整栈指针 (sp) 到正确的位置临时值的头部 (原本在 solts 头部, 现在跳过 args 和局部变量到临时区头部)
activation.regs().sp += len - script->nfixed();
// 清空 stackStorage, 释放内存
storage->setDenseInitializedLength(0);
}
// ========== 恢复 PC ==========
// 读取 resumeIndex, 计算偏移量, 将偏移量设置给 PC
JSScript* script = callee->nonLazyScript();
uint32_t offset = script->resumeOffsets()[genObj->resumeIndex()];
activation.regs().pc = script->offsetToPC(offset);
// ========== 准备恢复执行的参数 ==========
// 在栈顶压入三个值, 供 yield/await 指令使用
//
// 1. arg: 传递给 generator 的值 (next(value) 中的 value)
// 2. genObj: generator 对象本身 (用于内部操作)
// 3. resumeKind: 恢复类型 (normal=0, throw=1, return=2)
activation.regs().sp += 3;
MOZ_ASSERT(activation.regs().spForStackDepth(activation.regs().stackDepth()));
activation.regs().sp[-3] = arg;
activation.regs().sp[-2] = ObjectValue(*genObj);
activation.regs().sp[-1] = resumeKind;
// ========== 标记 generator 为运行状态 ==========
// 将 RESUME_INDEX_SLOT 设为 RESUME_INDEX_RUNNING (INT32_MAX) 见下
genObj->setRunning();
// ========== 恢复完成 ==========
return true;
}
获取保存的作用域链数据
// https://searchfox.org/firefox-main/rev/7d8518812ea2bee7fdd0fa2da03ab479f0075e01/js/src/vm/GeneratorObject.h#83 JSObject& environmentChain() const { return getFixedSlot(ENV_CHAIN_SLOT).toObject(); }
创建新栈帧
// https://searchfox.org/firefox-main/rev/7d8518812ea2bee7fdd0fa2da03ab479f0075e01/js/src/vm/Stack-inl.h#852 inline bool InterpreterActivation::resumeGeneratorFrame(HandleFunction callee, HandleObject envChain) { InterpreterStack& stack = cx_->interpreterStack(); if (!stack.resumeGeneratorCallFrame(cx_, regs_, callee, envChain)) { return false; } MOZ_ASSERT(regs_.fp()->script()->compartment() == compartment_); return true; }
/** * 为恢复的 generator/async 函数创建新的栈帧 * cite: https://searchfox.org/firefox-main/rev/7d8518812ea2bee7fdd0fa2da03ab479f0075e01/js/src/vm/Stack-inl.h#308 * * 核心任务: 在调用栈上分配并初始化新栈帧 * * @param cx JavaScript 上下文 * @param regs 当前寄存器状态 (包含 fp, sp, pc) * @param callee generator/async 函数对象 * @param envChain 保存的作用域链 (从 genObj 恢复) * @return 成功返回 true, 失败 (如栈溢出) 返回 false */ MOZ_ALWAYS_INLINE bool InterpreterStack::resumeGeneratorCallFrame( JSContext* cx, InterpreterRegs& regs, HandleFunction callee, HandleObject envChain) { // ========== 前置检查和准备 ========== // 确保这是 generator 或 async 函数 MOZ_ASSERT(callee->isGenerator() || callee->isAsync()); // 获取脚本对象 (包含字节码) 并保护以防止被 GC RootedScript script(cx, callee->nonLazyScript()); // 保存 generator 的调用者的信息以便下次抛出时返回 InterpreterFrame* prev = regs.fp(); // 调用者的栈帧 (如 gen.next() 的栈帧) jsbytecode* prevpc = regs.pc; // 调用者的程序计数器 Value* prevsp = regs.sp; // 调用者的栈指针 // 确保有调用者栈帧 (generator 不能在空栈上恢复) MOZ_ASSERT(prev); // ========== 标记内存分配起点 ========== // 在 LifoAlloc 内存分配器上做标记, 如果分配失败后续可以返回这个点的内存状态 LifoAlloc::Mark mark = allocator_.mark(); // ========== 验证函数类型 ========== // Generator 和 async 函数不能作为构造函数使用 MOZ_ASSERT(!callee->isConstructor()); // ========== 计算需要的内存大小 ========== // 注意在这里我们需要新建的是 "执行上下文" 需要的空间, 这里我不想展开介绍 执行上下文 的概念 // 可以简单的理解: 执行上下文 = callee + this + args... + 栈帧 // 上下文空间的内存分配 // ┌────────────────────────┐ ← 上下文开始 // │ Value: callee │ ╲ // ├────────────────────────┤ ╲ // │ Value: this │ } 调用环境 // ├────────────────────────┤ ╱ // │ Value: args... │ ╱ // ├────────────────────────┤ ← InterpreterFrame 栈帧开始 // │ ===InterpreterFrame=== │ ╲ // │ script_ │ ╲ // │ pc_ │ } 执行状态 // │ environmentChain_ │ ╱ // │ ... │ ╱ // ├────────────────────────┤ ← 动态区, 运行时数据 // │ Value: arg0 │ ← 参数 (这里的 arg 组和上方 args 指向同一内存 // │ Value: arg1 │ ← 参数 // │ Value: local0 │ ← 局部变量 // │ Value: temp0 │ ← 临时变量 // │ Value: temp1 │ ← 临时变量 // └────────────────────────┘ ← 上下文结束, 栈帧结束 // 计算形参数量 unsigned nformal = callee->nargs(); // 计算 callee + this + args + 动态区 总共需要的空间 // nvals = 2 (callee + this) + nformal (参数) + nslots (局部变量 + 临时值) unsigned nvals = 2 + nformal + script->nslots(); // ========== 分配上下文空间 ========== // 分配内存: InterpreterFrame 结构体 + nvals 个 Value // 这就暂停章节说的 Struct Hack: 在一次分配中包含固定结构和可变数组 uint8_t* buffer = allocateFrame(cx, sizeof(InterpreterFrame) + nvals * sizeof(Value)); // 检查分配是否成功 if (!buffer) { return false; } // ========== 初始化参数区域 ========== // 计算 argv 头的位置 // buffer + 2 * sizeof(Value) 跳过 callee 和 this Value* argv = reinterpret_cast<Value*>(buffer) + 2; // 在 argv 前 2 个 Value 处放置 callee argv[-2] = ObjectValue(*callee); // 在 argv 前 1 个 Value 处放置 this = undefined // 冷知识: 生成器函数的 this = undefined, 即使通过 obj.gen() 调用, 内部 this 也是 undefined argv[-1] = UndefinedValue(); // 将所有 argv 初始化为 undefined // 这里先只做初始化, 防止 GC 扫描到未初始化的内存 // 后期恢复时参数值会从 stackStorage 中复制过来 SetValueRangeToUndefined(argv, nformal); // ========== 初始化栈帧 ========== // 计算栈帧结构体的位置 // argv + nformal 跳过所有参数, 指向 InterpreterFrame 的起始位置 InterpreterFrame* fp = reinterpret_cast<InterpreterFrame*>(argv + nformal); // 保存内存分配的标记 (用于后续释放) fp->mark_ = mark; // 初始化元数据 (实现见下) fp->initCallFrame(prev, prevpc, prevsp, *callee, script, argv, 0, NO_CONSTRUCT); fp->resumeGeneratorFrame(envChain); // ========== 第八步: 更新寄存器状态 ========== // 准备寄存器以执行新栈帧 (见下) regs.prepareToRun(*fp, script); return true; }
总结一下上下文空间构建过程
低地址 ┌─────────────────────────┐ ← buffer (uint8_t*) │ Value: callee │ ← argv[-2] ├─────────────────────────┤ │ Value: this (undefined) │ ← argv[-1] ├─────────────────────────┤ │ Value: arg0 (undefined) │ ← argv[0] (参数区) │ Value: arg1 (undefined) │ ← argv[1] │ ... │ │ Value: argN-1 │ ← argv[nformal-1] ├─────────────────────────┤ ← fp (InterpreterFrame*) │ ===== 元数据区 ===== │ │ JSScript* script_ │ │ jsbytecode* pc_ │ │ JSObject* envChain_ │ ← 从 genObj 恢复 │ InterpreterFrame* prev_ │ ← 指向调用者 │ uint32_t flags_ │ │ Value* argv_ │ ← 指向上面的 argv │ LifoAlloc::Mark mark_ │ │ ... │ ├─────────────────────────┤ ← fp->slots() = fp + 1 │ ===== 动态数据区 ===== │ │ slots[0] (局部变量1) │ ← 将从 stackStorage 恢复 │ slots[1] (局部变量2) │ │ slots[2] (临时值1) │ │ ... │ │ slots[n] (临时值n) │ └─────────────────────────┘ ← regs.sp (初始位置) 高地址
初始化元数据过程
下面的代码分成部分
- 代码块1: 恢复
argv
,script_
等元数据 - 代码块2, 3: 赋值作用域链
- 代码块4: 重建
pc
等状态
// https://searchfox.org/firefox-main/rev/7d8518812ea2bee7fdd0fa2da03ab479f0075e01/js/src/vm/Stack-inl.h#48 inline void InterpreterFrame::initCallFrame(InterpreterFrame* prev, jsbytecode* prevpc, Value* prevsp, JSFunction& callee, JSScript* script, Value* argv, uint32_t nactual, MaybeConstruct constructing) { MOZ_ASSERT(callee.baseScript() == script); /* Initialize stack frame members. */ flags_ = 0; if (constructing) { flags_ |= CONSTRUCTING; } argv_ = argv; // 参数数组起始位置 script_ = script; // 脚本对象 (字节码) nactual_ = nactual; // 0 envChain_ = callee.environment(); prev_ = prev; // 调用者栈帧 (gen.next() 的栈帧) prevpc_ = prevpc; // 调用者的 PC prevsp_ = prevsp; // 调用者的 PC if (script->isDebuggee()) { setIsDebuggee(); } initLocals(); }
// https://searchfox.org/firefox-main/rev/7d8518812ea2bee7fdd0fa2da03ab479f0075e01/js/src/vm/Stack-inl.h#852 inline bool InterpreterActivation::resumeGeneratorFrame(HandleFunction callee, HandleObject envChain) { InterpreterStack& stack = cx_->interpreterStack(); // 恢复作用域链 (见下) if (!stack.resumeGeneratorCallFrame(cx_, regs_, callee, envChain)) { return false; } MOZ_ASSERT(regs_.fp()->script()->compartment() == compartment_); return true; }
// https://searchfox.org/firefox-main/rev/7d8518812ea2bee7fdd0fa2da03ab479f0075e01/js/src/vm/Stack.h#649 void resumeGeneratorFrame(JSObject* envChain) { MOZ_ASSERT(script()->isGenerator() || script()->isAsync()); MOZ_ASSERT_IF(!script()->isModule(), isFunctionFrame()); flags_ |= HAS_INITIAL_ENV; envChain_ = envChain; }
void prepareToRun(InterpreterFrame& fp, JSScript* script) { pc = script->code(); // PC 指向脚本开始, 后续会调整到恢复点 sp = fp.slots() + script->nfixed(); // 栈指针指向固定区之后 (临时区起始) fp_ = &fp; // 当前栈帧指针指向新栈帧 }
- 代码块1: 恢复
将保存的
slots
复制回新栈帧的动态数据区// https://searchfox.org/firefox-main/rev/7d8518812ea2bee7fdd0fa2da03ab479f0075e01/js/src/vm/Stack-inl.h#199 inline void InterpreterFrame::restoreGeneratorSlots(ArrayObject* src) { // 验证内存可用 MOZ_ASSERT(script()->nfixed() <= src->length()); MOZ_ASSERT(src->length() <= script()->nslots()); // 验证数组是完整连续的 MOZ_ASSERT(src->getDenseInitializedLength() == src->length()); // 获取 ArrayObject 头指针 const Value* srcElements = src->getDenseElements(); // memcpy mozilla::PodCopy(slots(), srcElements, src->length()); }
恢复 PC 与重置
RESUME_INDEX_SLOT
// 把顶层方法里的实现搬来重新强调一下 JSScript* script = callee->nonLazyScript(); uint32_t offset = script->resumeOffsets()[genObj->resumeIndex()]; activation.regs().pc = script->offsetToPC(offset);
// https://searchfox.org/firefox-main/rev/7d8518812ea2bee7fdd0fa2da03ab479f0075e01/js/src/vm/GeneratorObject.h#140 void setRunning() { MOZ_ASSERT(isSuspended()); setFixedSlot(RESUME_INDEX_SLOT, Int32Value(RESUME_INDEX_RUNNING)); }
总结
挂起
flowchart TD Start([执行到 yield/await]) --> CheckPC{检查 PC 指令} CheckPC -->|InitialYield/Yield/Await| GetGenObj[获取 GeneratorObject] CheckPC -->|其他指令| Error[断言失败] GetGenObj --> CheckStorage{检查 stackStorage} CheckStorage -->|已存在且非空| Error2[断言失败] CheckStorage -->|可用| CheckValues{nvalues > 0?} CheckValues -->|是| SaveSlots[保存动态数据] CheckValues -->|否| SavePC[保存 PC] SaveSlots --> GetStorage[获取 stackStorage 数组] GetStorage --> CheckCapacity{容量足够?} CheckCapacity -->|是| CopySlots[复制 slots 到数组] CheckCapacity -->|否| Error3[断言失败] CopySlots --> SlotsDetail[frame.slots → stackStorage
参数, 局部变量, 临时值] SlotsDetail --> SavePC SavePC --> ExtractIndex[从 PC 提取 yield 索引] ExtractIndex --> SetResumeIndex[设置 resumeIndex] SetResumeIndex --> ResumeDetail[genObj.RESUME_INDEX_SLOT
= yield 操作数] ResumeDetail --> SaveEnv[保存作用域链] SaveEnv --> SetEnvChain[设置 environmentChain] SetEnvChain --> EnvDetail[genObj.ENV_CHAIN_SLOT
= frame.environmentChain] EnvDetail --> Complete[挂起完成] Complete --> DestroyFrame[栈帧销毁] DestroyFrame --> ReturnCaller[返回调用者] style Start fill:#e1f5ff style Complete fill:#d4edda style Error fill:#f8d7da style Error2 fill:#f8d7da style Error3 fill:#f8d7da style SaveSlots fill:#cfe2ff style SavePC fill:#cfe2ff style SaveEnv fill:#cfe2ff恢复
flowchart TD Start([调用 gen.next/throw/return]) --> CheckState{检查 generator 状态} CheckState -->|Suspended| GetData[获取保存的数据] CheckState -->|Running/Closed| Error[抛出错误] GetData --> PrepareData["准备恢复数据
callee, envChain, arg, resumeKind"] PrepareData --> CreateFrame[创建新栈帧] CreateFrame --> SaveCaller[保存调用者信息] SaveCaller --> CallerDetail["prev = regs.fp
prevpc = regs.pc
prevsp = regs.sp"] CallerDetail --> AllocMem[分配内存] AllocMem --> CalcSize["计算大小:
sizeof(Frame) + nvals × sizeof(Value)"] CalcSize --> Alloc{分配成功?} Alloc -->|是| InitArgs["初始化 argv 区域"] Alloc -->|否| Error2[返回 false] InitArgs --> SetCallee["argv[-2] = callee"] SetCallee --> SetThis["argv[-1] = undefined"] SetThis --> InitArgv["argv[0..n] = undefined"] InitArgv --> ArgsNote["注意: 这些 undefined 是占位符
真实值在 slots 里"] ArgsNote --> InitFrame[初始化 InterpreterFrame] InitFrame --> SetMetadata[设置元数据] SetMetadata --> MetaDetail["script, prev, prevpc, prevsp
argv, flags, mark"] MetaDetail --> SetEnv[设置 environmentChain] SetEnv --> EnvDetail[从 genObj 恢复作用域链] EnvDetail --> PrepareRegs[准备寄存器] PrepareRegs --> RegDetail["fp = 新栈帧
sp = slots + nfixed
pc = script.code"] RegDetail --> RestoreSlots{有 stackStorage?} RestoreSlots -->|是| CopyData[恢复 slots 数据] RestoreSlots -->|否| RestorePC CopyData --> PodCopy["PodCopy: stackStorage → slots"] PodCopy --> DataDetail["恢复参数, 局部变量, 临时值
slots[0]=arg0, slots[1]=arg1..."] DataDetail --> AdjustSP[调整栈指针] AdjustSP --> SPDetail["sp += len - nfixed
跳过已恢复的临时值"] SPDetail --> ClearStorage[清空 stackStorage] ClearStorage --> RestorePC[恢复程序计数器] RestorePC --> GetOffset[获取字节码偏移量] GetOffset --> OffsetDetail["offset = resumeOffsets[resumeIndex]"] OffsetDetail --> SetPC[设置 PC] SetPC --> PCDetail["pc = script.offsetToPC(offset)
指向 yield/await 位置"] PCDetail --> PushArgs[压入恢复参数] PushArgs --> Push3["sp[-3] = arg
sp[-2] = genObj
sp[-1] = resumeKind"] Push3 --> ArgsUsage["字节码根据 resumeKind 决定:
normal: 正常恢复
throw: 抛出异常
return: 提前返回"] ArgsUsage --> SetRunning[设置运行状态] SetRunning --> RunDetail[resumeIndex = RESUME_INDEX_RUNNING] RunDetail --> Complete[恢复完成] Complete --> Execute[继续执行字节码] style Start fill:#e1f5ff style Complete fill:#d4edda style Error fill:#f8d7da style Error2 fill:#f8d7da style CreateFrame fill:#cfe2ff style RestoreSlots fill:#cfe2ff style RestorePC fill:#cfe2ff style PushArgs fill:#cfe2ff style ArgsNote fill:#fff3cd style DataDetail fill:#d1ecf1 style MetaDetail fill:#d1ecf1 style SPDetail fill:#d1ecf1 style PCDetail fill:#d1ecf1 style ArgsUsage fill:#d1ecf1数据流对比
flowchart LR subgraph Suspend["挂起过程 (Suspend)"] S1[栈上栈帧] -->|复制| S2[GeneratorObject] S2 --> S3[PC → resumeIndex] S2 --> S4[slots → stackStorage] S2 --> S5[envChain → ENV_CHAIN_SLOT] S3 & S4 & S5 --> S6[栈帧销毁] end subgraph Resume["恢复过程 (Resume)"] R1[GeneratorObject] -->|读取| R2[创建新栈帧] R2 --> R3[resumeIndex → PC] R2 --> R4[stackStorage → slots] R2 --> R5[ENV_CHAIN_SLOT → envChain] R3 & R4 & R5 --> R6[继续执行] end S6 -.->|gen.next| R1 style S1 fill:#ffebee style S6 fill:#ffcdd2 style R2 fill:#e8f5e9 style R6 fill:#c8e6c9
3.3 两种实现的对比
Babel 状态机 | SpiderMonkey 协程 | |
---|---|---|
暂停方式 | return 退出函数 | 保存栈帧 |
执行状态 | context.n 状态机 | RESUME_INDEX_SLOT 状态机 |
恢复方式 | switch 跳转 | 恢复 PC |
代码转换 | 需要 (转成 switch-case) | 不需要 (字节码指令) |
性能 | 较慢 | 更快 |
Part 4: 生成器的三种应用
生成器是一个强大的工具, 在不同场景下可被用于解决不同的问题
4.1 应用一: 作为控制流工具 (协程视角)
生成器满足协程的如下特性
控制权可转移: 函数可以主动让出控制权
状态可保持: 每次恢复都从上次暂停处继续
双向通信: 可以传入数据和接收数据
错误处理: 支持外部抛入异常
可以用来实现复杂控制流管理能力
应用场景
应用举例1: 协作式任务调度
// 生成器模拟耗时任务 - 任务A function* taskA() { console.log('任务A: 步骤1'); yield; console.log('任务A: 步骤2'); yield; console.log('任务A: 步骤3'); yield; console.log('任务A: 完成'); } // 生成器模拟耗时任务 - 任务B function* taskB() { console.log('任务B: 步骤1'); yield; console.log('任务B: 步骤2'); yield; console.log('任务B: 完成'); } // 生成器模拟耗时任务 - 任务C function* taskC() { console.log('任务C: 步骤1'); yield; console.log('任务C: 完成'); } // 任务调度器 - 轮询执行每个任务的子任务 class TaskScheduler { constructor() { this.tasks = []; this.currentTaskIndex = 0; } addTask(taskGenerator) { this.tasks.push(taskGenerator); } run() { while (this.tasks.length > 0) { const task = this.tasks[this.currentTaskIndex]; const result = task.next(); if (result.done) { // 任务完成, 从队列中移除 this.tasks.splice(this.currentTaskIndex, 1); // 调整索引 if (this.currentTaskIndex >= this.tasks.length) { this.currentTaskIndex = 0; } } else { // 任务未完成, 切换到下一个任务 this.currentTaskIndex = (this.currentTaskIndex + 1) % this.tasks.length; } } console.log('所有任务完成'); } } // 使用调度器 const scheduler = new TaskScheduler(); scheduler.addTask(taskA()); scheduler.addTask(taskB()); scheduler.addTask(taskC()); scheduler.run(); // 输出: // 任务A: 步骤1 // 任务B: 步骤1 // 任务C: 步骤1 // 任务A: 步骤2 // 任务B: 步骤2 // 任务C: 完成 // 任务A: 步骤3 // 任务B: 完成 // 任务A: 完成 // 所有任务完成
应用举例2: 可交互的任务执行
// 可暂停可执行的数据处理器 function* processLargeDataset(data) { let result = []; for (let i = 0; i < data.length; i++) { const processed = expensiveOperation(data[i]); result.push(processed); if (i % 100 === 0) { const command = yield { type: 'progress', progress: i / data.length, processed: i, total: data.length }; if (command === 'pause') { while ((yield { type: 'paused' }) !== 'resume') { } } else if (command === 'cancel') { return { cancelled: true, result, processed: i }; } } } return { cancelled: false, result, processed: data.length }; } // 使用 function expensiveOperation(item) { let sum = 0; for (let i = 0; i < 1000000; i++) { sum += Math.sqrt(i) * item; } return sum; } const data = Array.from({ length: 10000 }, (_, i) => i); const processor = processLargeDataset(data); let nextCommand = "run"; // 模拟用户操作 setTimeout(() => { console.log('\n用户点击了暂停'); nextCommand = 'pause'; }, 2000); setTimeout(() => { console.log('\n用户点击了继续'); nextCommand = 'resume'; }, 5000); function step() { const result = processor.next(nextCommand); if (result.done) { console.log('处理完成:', result.value); return; } if (result.value.type === 'progress') { console.log(`进度: ${(result.value.progress * 100).toFixed(2)}% (${result.value.processed}/${result.value.total})`); } else if (result.value.type === 'paused') { console.log('已暂停, 等待恢复...'); } nextCommand = "run"; setTimeout(step, 0); } step(); // 进度: 0.00% (0/10000) // 进度: 1.00% (100/10000) // ... // 已暂停, 等待恢复... // ... // 用户点击了继续 // 进度: 14.00% (1400/10000) // ...
应用举例3: 状态机实现
// TCP连接状态机 function* tcpConnectionStateMachine() { console.log('[CLOSED] 初始状态'); // 等待 OPEN 命令 let command = yield 'CLOSED'; if (command !== 'OPEN') { throw new Error('期望 OPEN 命令'); } console.log('[LISTEN] 开始监听'); command = yield 'LISTEN'; if (command === 'SYN_RECEIVED') { console.log('[SYN_RECEIVED] 收到 SYN'); command = yield 'SYN_RECEIVED'; if (command === 'ACK') { console.log('[ESTABLISHED] 连接建立'); // 连接保持, 可以传输数据 while (true) { command = yield 'ESTABLISHED'; if (command === 'CLOSE') { console.log('[FIN_WAIT_1] 开始关闭'); command = yield 'FIN_WAIT_1'; if (command === 'ACK') { console.log('[FIN_WAIT_2] 等待对方关闭'); command = yield 'FIN_WAIT_2'; if (command === 'FIN') { console.log('[TIME_WAIT] 等待超时'); yield 'TIME_WAIT'; console.log('[CLOSED] 连接关闭'); return 'CLOSED'; } } } else if (command === 'DATA') { console.log('[ESTABLISHED] 传输数据'); // 继续保持 ESTABLISHED 状态 } } } } } // 使用状态机 const connection = tcpConnectionStateMachine(); console.log(connection.next().value); // CLOSED console.log(connection.next('OPEN').value); // LISTEN console.log(connection.next('SYN_RECEIVED').value); // SYN_RECEIVED console.log(connection.next('ACK').value); // ESTABLISHED console.log(connection.next('DATA').value); // ESTABLISHED (传输数据) console.log(connection.next('DATA').value); // ESTABLISHED (传输数据) console.log(connection.next('CLOSE').value); // FIN_WAIT_1 console.log(connection.next('ACK').value); // FIN_WAIT_2 console.log(connection.next('FIN').value); // TIME_WAIT console.log(connection.next().value); // CLOSED
4.2 应用二: 作为数据转换工具 (函数式编程视角)
函数式编程核心原则
在函数式编程中, 我们强调:
- 纯函数: 无副作用, 相同输入总是产生相同输出
- 不可变性: 不修改原数据, 总是返回新数据
- 声明式: 描述"做什么"而非"怎么做"
- 组合性: 小函数组合成大函数
生成器天然符合这些原则 (如果正确使用的话) .
Iterator Helpers API (MDN)
ES2026 新增了一些迭代器的API, 让生成器具有了更强的函数式编程能力, 这里会简单介绍一些
兼容性说明: 除
[Symbol.dispose]()
外的所有 API 已在所有 JavaScript 引擎的最新版本中支持
Iterator
是一个内置的抽象类, 所有 JavaScript 内置迭代器都继承自它. 它提供了丰富的辅助方法来处理迭代器.在 ES2026 之前, 迭代器仅仅是一个实现了迭代器协议的对象, 即: 只要有
next()
方法就够了. 但这样的"裸迭代器"无法使用任何辅助方法有了
Iterator
类之后, 我们创建自定义迭代器时应该继承它, 这样就成为了 恰当的迭代器 (Proper iterators, MDN), 既符合迭代器协议, 又拥有所有辅助方法这里的 "迭代器" 和 "恰当的迭代器" 的关系就像 "类数组" 和 "数组" 的关系
// 类数组对象: 有 length 和索引, 但不是真数组 const arrayLike = { length: 2, 0: 'a', 1: 'b' }; // arrayLike.map(x => x.toUpperCase()) // TypeError // 数组: 可以使用所有数组方法 const realArray = ['a', 'b']; // realArray.map(x => x.toUpperCase()) // ['A', 'B']
使用 "恰当的迭代器" 可以更加优雅地应用迭代器的强大功能
Iterator.from()
(MDN, chrome 122+)将具有
next
方法的普通迭代器转为继承自Iterator
的恰当的迭代器输入: 可迭代对象. 输出: 恰当的迭代器
输入: 迭代器. 输出: 恰当的迭代器
map()
(MDN, chrome 122+)类似数组的
map
, 返回迭代器function* range(st, ed) { for (let i = st; i < ed; i++) { yield i; } } const doubled = range(1, 5).map((x) => x * 2); console.log(...doubled); // 2 4 6 8
flatMap()
(MDN, chrome 122+)类似数组的
map
然后flat(1)
, 返回迭代器const arr1 = [["a", 1], ["b", 2]]; const arr2 = [["c", 3], ["d", 4]]; const mapAndFlat = [arr1, arr2].flatMap((it) => it); // 两次的 it 是 arr1, arr2 console.log(mapAndFlat); // [["a", 1], ["b", 2], ["c", 3], ["d", 4]]
filter()
(MDN, chrome 122+)类似数组的
filter
, 只不过返回的是迭代器// 使用 map 例子中的 range() const odd = range(1, 5).filter((x) => x % 2); console.log(...odd); // 1 3
drop()
(MDN, chrome 122+)丢弃迭代器的前
n
项. 返回迭代器// 使用 map 例子中的 range() const fromThirdNumber = range(1, 5).drop(2); console.log(...fromThirdNumber); // 3 4
take()
(MDN, chrome 122+)取迭代器的前
n
项, 返回迭代器// 使用 map 例子中的 range() const firstTwoNumber = range(1, 5).take(2); console.log(...firstTwoNumber); // 1 2
常被用于提取无限序列的几项
// 产生无限长度 fib 的生成器 function* fib() { let a = 1, b = 1; while (true) { yield a; [a, b] = [b, a + b]; } } const fibIter = fib(); const firstTenFibs = fibIter.take(10); console.log(...firstTenFibs); // 1 1 2 3 5 8 13 21 34 55
every()
(MDN, chrome 122+)类似数组的
every
, 也返回bool
// 使用 map 例子中的 range() const allPositive = range(1, 10).every(it => it > 0); console.log(allPositive); // true
find()
(MDN, chrome 122+)类似数组的
find
, 也返回迭代器元素// 使用 map 例子中的 range() const firstOdd = range(0, 10).find(it => it % 2); console.log(firstOdd); // 1
reduce()
(MDN, chrome 122+)类似数组的
reduce
// 使用 map 例子中的 range() const sum = range(1, 10).reduce((acc, val) => acc + val, 0); console.log(sum); // 45
forEach()
(MDN, chrome 122+)类似数组的
forEach
// 使用 map 例子中的 range() range(1, 5).forEach(it => console.log(it)); // 1 2 3 4
toArray()
(MDN, chrome 122+)将迭代器转为数组 (类似
[...iter]
)// 使用 map 例子中的 range() console.log(range(1, 5).toArray()); // [1, 2, 3, 4]
[Symbol.dispose]()
(MDN, chrome 134+ 部分运行时不支持)执行迭代器
return()
方法, 引入这个函数的目的是为了让迭代器可以被using
管理 (开发者可以用using x
表明对资源x
的引用, 当程序离开using
所属作用域时, 程序会自动调用x[Symbol.dispose]()
销毁资源. 即 RAII)function* generateNumbers() { try { yield 1; yield 2; yield 3; } finally { console.log("clean up"); } } { using gen = generateNumbers(); console.log(gen.next().value); } // 1 // clean up
应用场景 - 通过函数组合实现数据管道
// 日志分析工具
// 从日志中获取 ERROR 级别的 message, 并统计 message 出现频次最高的 10 条数据
function analyzeLogsWithHelpers(logs) { // logs 是数组
const topErrors = logs
.values() // 转为迭代器
.filter(log => log.level === 'ERROR')
.map(log => log.message)
.reduce((acc, msg) => {
acc[msg] = (acc[msg] || 0) + 1;
return acc;
}, {});
return Object.entries(topErrors)
.sort((a, b) => b[1] - a[1])
.slice(0, 10);
}
// 找到 fib 中第 5 个偶数
function* fib() {
let a = 1, b = 1;
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
const fifthEvenFibonacci = fib().filter(it => it % 2 === 0)
.drop(4)
.find(_ => true);
console.log(fifthEvenFibonacci); // 610
与数组的区别
既然迭代器的这些方法都能在数组中找到对应, 那为什么要再发明一套迭代器方法而不是在实践中直接
iter.toArray()
转成数组呢?答案就在迭代器的特殊执行方式实现的惰性求值与管道执行, 我们会在下节介绍
4.3 应用三: 性能优化工具 (惰性求值视角)
基本概念
惰性求值 (Lazy Evaluation)
只在真正需要结果时才进行计算. 与之相对的是"饥饿求值 (Eager Evaluation)", 它会立即计算所有结果
// 饥饿求值 const eagerResult = data.eagerProcess(); // 完成计算 console.log(eagerResult) // 仅输出
// 惰性求值 const lazyResult = data.lazyProcess(); // 什么都不做 console.log(eagerResult) // 此时计算并输出
流水线式处理 (Pipeline Processing)
在对一组数据做多重变换时, 将一个数据完整的做完整条转换链路后再处理另一个. 与之对应的是 "批处理模式", 将所有的数据批量应用多次变化
// 批处理模式 const batResult = data.double() // 全部数据 *2, 同时产生数据 .slice(3) // 移出前三组数据, 同时生成新数据 .double() // 全部数据 *2, 同时产生数据
// 流水线模式 // 想像有一个流水线 // [产出data中的数据] -> [×2加工节点] -> [移出前三个到达的元素节点] -> [×2加工节点] -> [结束] // 元素一个一个的从头蹦出来, 走一遍流程, 存入结果 const batResult = data.double() // 全部数据 *2, 同时产生数据 .slice(3) // 移出前三组数据, 同时生成新数据 .double() // 全部数据 *2, 同时产生数据
验证惰性求值和饥渴求值
饥渴求值
可以看到,
map
先把所有元素处理完, 然后filter
再处理所有元素const arr = [1, 2, 3, 4, 5]; const eagerResult = arr .map(x => { console.log(`map: ${x}`); return x * 2; }) .filter(x => { console.log(`filter: ${x}`); return x > 5; }); console.log('Eager Result:', eagerResult); // map: 1 // map: 2 // map: 3 // map: 4 // map: 5 // filter: 2 // filter: 4 // filter: 6 // filter: 8 // filter: 10 // Eager Result: [ 6, 8, 10 ]
惰性求值
可以看到, 每个元素都是 "流水线式" 处理: 先读取, 在
map
, 最后filter
. 一个元素处理完才处理下一个function* lazySequence() { for (let x of [1, 2, 3, 4, 5]) { console.log(`Read ${x}`); yield x; } } const lazyResult = lazySequence() .map(x => { console.log(`Map ${x}`); return x * 2; }) .filter(x => { console.log(`Filter ${x}`); return x > 5; }); console.log("Lazy evaluation:"); console.log([...lazyResult]); // Lazy evaluation: // Read 1 // Map 1 // Filter 2 // Read 2 // Map 2 // Filter 4 // Read 3 // Map 3 // Filter 6 // Read 4 // Map 4 // Filter 8 // Read 5 // Map 5 // Filter 10 // [ 6, 8, 10 ]
验证流水线处理
function* userSequence() {
for (let i = 1; i <= 5; i++) {
yield { getType: () => 'user', id: i };
}
yield { getType: () => 'admin', id: 6 };
yield { id: 7 }; // 有问题的用户对象
}
// 流水线式处理, 一旦达成就给流水线停掉, 不会访问到有问题的 7 用户
const pipelineFindAdminId = userSequence()
.map(it => ({ ...it, type: it.getType() }))
.find(it => it.type === 'admin')
.id;
console.log('Pipeline Admin ID:', pipelineFindAdminId); // Pipeline Admin ID: 6
// 批处理式处理, 第一步 map 时就会访问到有问题的 7 用户
const batUserList = [...userSequence()];
const batFindAdminId = batUserList
.map(it => ({ ...it, type: it.getType() }))
.find(it => it.type === 'admin')
.id;
console.log('Eager Admin ID:', batFindAdminId); // TypeError: it.getType is not a function
惰性求值与流水线处理的好处
- 避免不必要的计算
- 支持处理 流式 / 无限 序列
- 内存效率高
CPU 缓存友好 (仅流水线处理具备, 流水线模式对每个元素连续处理, 数据在 CPU 缓存中的局部性更好) (理论上是这样的但我无法验证)
应用场景:
这几个场景其实用的都是一个特性, 不应该分开来写这么多的...
仅做必要计算
function* longSequence() { for (let i = 0; i < 1e6; i++) { yield i; } } // 第一个大于 855 的整数平方根 const result = longSequence() .map(it => Math.sqrt(it)) // map 到 855**2+1 就停下了, 不会继续算 sqrt .find(it => it > 855); console.log(result) // 855.0005847951217
无限序列
function* fib() { let a = 1, b = 1; while (true) { yield a; [a, b] = [b, a + b]; } } // 大于 100 的 3 个斐波那契数 const fibsGreaterThan100 = fib().filter(n => n > 100).take(3); console.log(...fibsGreaterThan100); // 144, 233, 377
内存优化案例1
仅存储迭代逻辑和必要数据
function* lazyRange(n) { for (let i = 0; i < n; i++) { yield i; } } const lazy = lazyRange(1e9); // 几百字节, 按需生成
内存优化案例2
// 实现 zero-copy 的合并两个大数组并读取十个元素 const bigArr1 = Array.from({ length: 1e6 }, (_) => Math.random()); const bigArr2 = Array.from({ length: 1e6 }, (_) => Math.random()); // 使用迭代器 console.time('[iter] flatMap'); const iteratorFlatMap = [bigArr1, bigArr2].values().flatMap(set => set); console.timeEnd('[iter] flatMap'); console.log('结果类型:', iteratorFlatMap.constructor.name); console.time('[iter] first 10'); const iteratorFirst10 = iteratorFlatMap.take(10).toArray(); console.timeEnd('[iter] first 10'); console.log('取值结果:', iteratorFirst10); // 直接拷贝 console.time('[copy] ...'); const copiedArray = [...bigArr1, ...bigArr2]; console.timeEnd('[copy] ...'); console.log('结果类型:', copiedArray.constructor.name); console.time('[copy] first 10'); const copiedFirst10 = [...copiedArray].slice(0, 10); console.timeEnd('[copy] first 10'); console.log('取值结果:', copiedFirst10); // [iter] flatMap: 0.012ms // 结果类型: Iterator // [iter] first 10: 0.015ms // 取值结果: [...] // [copy] ...: 14.609ms // 结果类型: Array // [copy] first 10: 1.04ms // 取值结果: [...]
CPU 缓存优化失败
我不知道为什么迭代器反而比数组慢...也举不出一个合适的例子, 抱歉...
const SIZE = 1e6 // 两种方式都预先分配好所有数组, 消除内存分配因素 const data = new Float64Array(SIZE); for (let i = 0; i < SIZE; i++) { data[i] = i; } const preallocatedIter = new Float64Array(SIZE); const preallocatedArray = new Float64Array(SIZE); console.time('array'); data.forEach((value) => { Math.log(value ** 1.2345); return value; }); data.forEach((value) => { Math.log(value ** 2.3456); return value; }); data.forEach((value) => { Math.log(value ** 3.4567); return value; }); data.forEach((value) => { Math.log(value ** 4.5678); return value; }); data.forEach((value) => { Math.log(value ** 5.6789); return value; }); data.forEach((value, index) => { preallocatedArray[index] = Math.log(value) }); console.timeEnd('array'); const iter = data[Symbol.iterator](); console.time('iter'); iter.map((value) => { Math.log(value ** 1.2345); return value; }) .map((value) => { Math.log(value ** 2.3456); return value; }) .map((value) => { Math.log(value ** 3.4567); return value; }) .map((value) => { Math.log(value ** 4.5678); return value; }) .map((value) => { Math.log(value ** 5.6789); return value; }) .forEach((value, index) => { preallocatedIter[index] = Math.log(value); }); console.timeEnd('iter'); // array: 38.91ms // iter: 70.46ms
Part 5: 总结
5.1 为什么在实践中很少看到复杂的生成器
生成器作为迭代器工厂很常见
class Range {
constructor(start, end) {
this.start = start;
this.end = end;
}
*[Symbol.iterator]() {
for (let i = this.start; i <= this.end; i++) {
yield i;
}
}
}
const range = new Range(1, 5);
console.log([...range]); // [1, 2, 3, 4, 5]
但作为协程很少见
function* complexFlow() {
const input1 = yield 'Ready for input 1';
const input2 = yield 'Ready for input 2';
const input3 = yield 'Ready for input 3';
return input1 + input2 + input3;
}
const gen = complexFlow();
console.log(gen.next()); // { value: 'Ready for input 1', done: false }
console.log(gen.next(10)); // { value: 'Ready for input 2', done: false }
console.log(gen.next(20)); // { value: 'Ready for input 3', done: false }
console.log(gen.next(30)); // { value: 60, done: true }
因为
- 消费方式不友好:
for...of
等边界语法只能无脑next
, 无法充分利用双向通信特性. 如果想利用生成器的完整特性就需要手动调用迭代器消费, 使用起来并不方便 - 现代
async
/await
语法提供了更好的异步方案
5.2 何时使用生成器
决策树
flowchart LR Start[需要生成序列?] Start -->|是| Lazy[需要惰性求值?] Start -->|否| Control[需要控制流管理?] Lazy -->|是| BigData[大数据集 / 无限序列?] Lazy -->|否| SmallData[小数据集?] BigData -->|是| Gen1[使用生成器
惰性求值特性] BigData -->|否| Pipeline[数据转换管道?] Pipeline -->|是| Gen2[使用生成器
函数式方法] Pipeline -->|否| Array1[使用数组方法] SmallData -->|是| Array2[使用数组方法] SmallData -->|否| Gen3[考虑生成器] Control -->|是| Async[异步操作?] Control -->|否| Normal1[使用普通函数] Async -->|是| AsyncAwait[使用 async/await] Async -->|否| StateMachine[状态机 / 任务调度?] StateMachine -->|是| Gen4[使用生成器
协程特性] StateMachine -->|否| Normal2[普通函数] style Gen1 fill:#90EE90 style Gen2 fill:#90EE90 style Gen3 fill:#90EE90 style Gen4 fill:#90EE90 style Array1 fill:#87CEEB style Array2 fill:#87CEEB style AsyncAwait fill:#FFA07A style Normal1 fill:#DDA0DD style Normal2 fill:#DDA0DD
场景举例
场景 | 方案 | 特性 | 原因 |
---|---|---|---|
遍历小数组 | 数组方法 | - | 简单直接, 性能足够 |
遍历大数组 | 生成器 | 惰性求值 | 内存效率高, 支持短路 |
无限序列 | 生成器 | 惰性求值 | 唯一选择 |
数据转换管道 | 生成器 + Iterator Helpers | 函数式 | 声明式, 可组合 |
文件流处理 | 生成器 | 惰性求值 | 不需要一次性加载 |
简单异步操作 | async/await | - | 语法更简洁 |
复杂异步流程 | async/await + 状态管理 | - | 更清晰 |
状态机 | 生成器 | 协程 | 天然支持状态转换 |
任务调度 | 生成器 | 协程 | 支持协作式调度 |
自定义迭代逻辑 | 生成器 | 迭代器工厂 | 比手写 next() 简洁 |
5.3 常见陷阱
生成器的一次性特性
生成器产生的迭代器只能遍历一次
function* numbers() { yield 1; yield 2; yield 3; } const gen = numbers(); console.log([...gen]); // [1, 2, 3] console.log([...gen]); // [] - 已经耗尽!
忽略
return
的特殊性return
的值不会出现在for...of
中function* withReturn() { yield 1; yield 2; return 3; // 注意这里是 return } // for...of 不会获取 return 的值 console.log([...withReturn()]); // [1, 2] // 只有手动调用 next() 才能看到 const gen = withReturn(); console.log(gen.next()); // { value: 1, done: false } console.log(gen.next()); // { value: 2, done: false } console.log(gen.next()); // { value: 3, done: true } - 注意 done: true
生成器的逻辑执行时序不确定, 不要在生成器中使用副作用, 否则会造成严重的时序混乱
let count = 0; function* impure() { count++; // 副作用 yield count; } // 难以预测和测试 const g1 = impure(); const g2 = impure(); g1.next(); // count = 1 g2.next(); // count = 2 (受 g1 影响)
function* pure() { let localCount = 0; while (true) { yield ++localCount; } } // 每个生成器独立 const g3 = pure(); const g4 = pure(); g3.next(); // 1 g4.next(); // 1 (独立)
避免在 React 的 props 中直接消费生成器对象
生成器对象的消费是带副作用的, 不应该在 React 中直接消费生成器
function* range() { for (let i = 0; i < 10; i++) yield i; } const seq = range();
// seq 是生成器返回值 // 结果: // count = 0 0123456789 // count = 1 (没了) // 陷阱: seq 只能被消费一次, 下次渲染时为空 function SequenceList({ seq }) { const [count, setCount] = useState(0); useEffect(() => { setTimeout(() => { setCount(1); }, 2000); }, []); return (<> <p>count = {count}</p> <div> {[...seq].map((num, idx) => ( <span key={idx}> {num} </span> ))} </div> </>); }
// 通过 useMemo / useEffect 就可以规避? // 结果: // count = 0 0123456789 // count = 1 0123456789 // 陷阱: 看起来解决了, 但是在严格模式下 useMemo / useEffect 会反复执行, 隐患更大 function SequenceList({ seq }) { const data = useMemo(()=>[...seq], [seq]) // 这是一个副作用! return ( <div className="sequence-box error"> <div className="items"> {[...data].map((num, idx) => ( <span key={idx} className="item">{num}</span> ))} </div> </div> ); }
// 通过 useMemo / useEffect 解决也有风险 // 结果: // count = 0 (完全没了) // count = 1 (完全没了) // 陷阱: 严格模式下 useMemo / useEffect 两次执行结果不同! // <React.StrictMode> // <div className='App'> // <SequenceList seq={seq}>Hello React.</SequenceList> // </div> // </React.StrictMode> function SequenceList({ seq }) { const data = useMemo(()=>[...seq], [seq]) return ( <div className="sequence-box error"> <div className="items"> {[...data].map((num, idx) => ( <span key={idx} className="item">{num}</span> ))} </div> </div> ); }
解决方案: 传递生成器和参数 / 提前将数据转换为可以纯净消费的可迭代对象
完结撒花~ (好复古的结束语 ε≡ヘ( ´∀`)ノ