在JavaScript中处理一连串异步任务时,回调嵌套会让代码迅速失控,Promise解决了大部分链式调用问题,但复杂逻辑依然需要大量then回调,代码结构依然不够直观。迭代器和生成器函数为异步流程提供了一种更接近同步风格的表达方式:生成器可以在yield处暂停,等待外部传入数据后继续执行,而这个数据可以来自异步操作的结果。本文重点分析如何基于迭代器协议和生成器函数构建可控的异步执行框架,并讨论其与async、await的关系。

一、迭代器协议与可迭代对象
迭代器是JavaScript中一种标准化的访问集合元素的机制。一个对象只要实现了next方法,并且该方法返回包含value和done两个属性的对象,就可以被称为迭代器。value表示本次迭代得到的值,done为布尔值,表示遍历是否结束。当done为true时,value通常可以省略或作为返回值。这种协议看似简单,但它把遍历逻辑从数据结构中解耦出来,任何来源的数据都可以包装成统一的迭代器形态。
可迭代对象则是更上一层的约定。一个对象如果实现了Symbol.iterator方法,并且该方法返回一个迭代器,那么这个对象就可以被for...of循环、扩展运算符以及解构赋值等语法消费。数组、字符串、Map和Set都内置了可迭代能力。自定义可迭代对象时,可以灵活控制遍历的节奏和次数,例如生成无限序列或者惰性计算的数据流。下面的代码定义了一个可迭代的范围对象,它并不一次性创建所有数字,而是在每次调用next时才计算当前值。
const range = {
from: 1,
to: 5,
[Symbol.iterator]() {
let current = this.from;
const last = this.to;
return {
next() {
if (current <= last) {
return { value: current++, done: false };
}
return { done: true };
}
};
}
};
for (const num of range) {
console.log(num); // 1 2 3 4 5
}
这里的关键点在于next方法返回的对象结构。外部for...of循环在内部反复调用next,直到done变为true。迭代器协议是同步的,但这为异步场景打下了基础:只要把value替换成一个异步任务的结果,并且让外部调度器在任务完成后再调用next,就能实现异步迭代的效果。生成器函数恰好提供了这种暂停和恢复的能力。
二、生成器函数:可暂停的执行模型
生成器函数的声明方式是在function关键字后加上星号,例如function*。函数内部使用yield关键字可以暂停执行,并把yield后面的值返回给调用方。调用生成器函数并不会立即执行函数体,而是返回一个生成器对象,这个对象同时实现了迭代器协议和可迭代协议。每次调用生成器对象的next方法,函数体就会从上次暂停的位置继续执行,直到遇到下一个yield或函数结束。
生成器最强大的特性是双向通信。next方法可以接收一个参数,这个参数会作为上一个yield表达式的返回值传递给生成器内部。这意味着外部代码可以在生成器暂停时向其注入数据,控制它的后续执行路径。下面的示例演示了这种双向数据交互,生成器先返回一个提示,然后外部代码传入用户输入,生成器再把输入拼接后返回。
function* conversation() {
const name = yield '请输入你的名字';
const age = yield '请输入你的年龄';
return `${name}今年${age}岁`;
}
const gen = conversation();
console.log(gen.next().value); // 请输入你的名字
console.log(gen.next('小明').value); // 请输入你的年龄
console.log(gen.next(20).value); // 小明今年20岁
从执行状态看,生成器内部维护着一个状态机,它在暂停、运行和完成三种状态之间切换。每次yield都会记录当前的代码位置和局部变量环境,因此函数可以在任意表达式中暂停而不丢失上下文。这种机制让生成器天然适合描述需要等待外部条件才能继续的逻辑,比如等待用户输入、等待文件读取完成、等待网络请求返回。把这些外部条件包装成异步任务,再配合一个聪明的调度器,就能用同步写法编写异步代码。
三、异步应用:生成器驱动Promise
基于生成器的异步方案核心思想是:在生成器内部用yield抛出Promise对象,外部的执行器负责调用该Promise的then方法,等待它resolve后把结果通过next传回生成器内部。这样生成器内部的代码看起来就像是在同步等待异步结果一样,每一行都是顺序执行的直觉。一个最简单的执行器可以这样实现:拿到生成器,手动地逐步调用next,并在Promise完成后继续。
function* fetchUser() {
const response = yield fetch('/api/user');
const user = yield response.json();
return user.name;
}
function run(genFunc) {
const gen = genFunc();
function step(nextResult) {
if (nextResult.done) {
return Promise.resolve(nextResult.value);
}
const promise = nextResult.value;
return Promise.resolve(promise).then(
result => step(gen.next(result)),
error => step(gen.throw(error))
);
}
return step(gen.next());
}
run(fetchUser).then(name => {
console.log('用户名:', name);
});
这段代码中的run函数就是微型的异步生成器运行时。它先调用gen.next启动生成器,得到第一个yield出来的fetch Promise。当Promise成功后,run通过gen.next把响应对象传回生成器,生成器继续执行到下一行response.json(),再次yield一个Promise。run继续等待,直到生成器返回最终结果。如果某个Promise失败,run调用gen.throw把错误抛入生成器内部,这样生成器中的try...catch就能捕获异步错误,不需要在每一层then里单独处理。
这种模式的价值在于错误处理可以像同步代码一样使用try...catch,而且多个异步操作之间的依赖关系直接通过变量赋值表达,不再需要维护复杂的链式then结构。例如要连续调用三个接口,第二个接口依赖第一个接口的结果,第三个接口依赖前两个的结果,用生成器写出来的代码与同步逻辑几乎没有区别。这在早期ES2017之前尤为珍贵,当时的著名库co就是基于这个思路实现了完整的异步流程控制。
四、与async/await对比及实际场景
async函数和await操作符可以看作是基于生成器和Promise的语法糖。async函数体在执行到await时会暂停,等待Promise完成后再继续,这与生成器的yield加外部执行器的工作方式高度一致。实际上,很多JavaScript引擎早期实现async/await时,就是先将其转译为生成器加自动执行器的组合。async/await的优点在于语法更简洁,不需要手动编写run调度器,而且函数返回的一定是Promise,错误处理更加标准化。
但生成器并没有因此失去价值。由于生成器的推进完全由外部调用next控制,它可以实现比async/await更灵活的调度策略。比如在并发控制中,async/await通常需要配合Promise.all或其他工具控制并发数量,而生成器可以随时暂停或恢复任务,天然适合构建任务队列。下面的示例实现了一个简单的并发控制器,它允许最多两个任务同时运行,其他任务在队列中等待。
function* taskQueue(tasks) {
const results = [];
for (const task of tasks) {
results.push(yield task());
}
return results;
}
function runWithLimit(genFunc, limit) {
const gen = genFunc();
let running = 0;
const queue = [];
function next() {
while (running < limit && !gen.done) {
running++;
const { value, done } = gen.next();
if (done) {
running--;
continue;
}
Promise.resolve(value).finally(() => {
running--;
next();
});
}
}
next();
return gen;
}
需要注意的是,上面的简化示例只演示了并发触发的概念,实际生产环境还需要处理结果收集和错误传播。生成器在异步场景下的真正优势是调度透明:你可以决定何时推进生成器、是否取消任务、如何在多个生成器之间轮流执行。对于需要细粒度流程控制的场景,例如分片上传、批量请求限速、复杂状态机驱动接口,生成器仍然是一种值得掌握的底层工具。
理解迭代器和生成器函数的异步应用,不仅能够帮助开发者阅读早期基于co库的遗留代码,也能加深对JavaScript执行模型和异步编程本质的认识。当你需要脱离async/await的固定节奏,实现自己的异步调度逻辑时,生成器提供的那份自由度和可组合性会显得格外宝贵。
JavaScript迭代器生成器函数异步编程修改时间:2026-09-27 21:45:20