小白也能搞懂的JavaScript Generator函数:异步控制流新姿势
那玩意儿我天天用,不就是 function* 加个 yield 嘛,还能 throw 和 return,配合 Promise 能实现异步流程控制,不过现在有 async/await 了,但理解原理对调试很有帮助…如果你用的执行器(比如 co)期望 yield 的都是 Promise,但你 yield 了个普通值,有些执行器会报错,有些会当成已 resolve 的 Promise 处理。如果执行器只
小白也能搞懂的JavaScript Generator函数:异步控制流新姿势
- 小白也能搞懂的JavaScript Generator函数:异步控制流新姿势
-
- 为啥我翻遍MDN还是看不懂Generator?别慌,你不是一个人
- 从"function*"说起:这星号不是装饰,是魔法开关
- yield不是return,它更像"暂停键+传话筒"
- next()调用就像按遥控器:播一帧、问一句、再继续
- 普通函数 vs Generator:一个跑完就歇,一个能随时插嘴
- 用for...of遍历Generator:比while循环优雅一万倍
- throw()和return():强行打断和体面退场的区别
- 同步代码写异步逻辑:不用async/await也能装大神
- 懒加载数据流:内存不爆、性能不崩的秘密武器
- 状态机实现神器:复杂流程控制不再靠if-else堆成山
- 浏览器兼容性翻车现场:IE?别提了,提就是泪
- 调试体验差到想砸键盘:断点打哪?call stack看懵
- 和Promise混用容易把自己绕进去:谁先谁后?谁resolve谁?
- 用Generator做请求重试机制:失败3次再放弃,稳得很
- 模拟无限滚动列表:滑到底自动yield新数据
- 配合Redux-Saga管理副作用:虽然现在没人用了但面试还考
- 为啥next()返回值是{ value: xxx, done: false }?结构拆解讲透
- yield后面跟表达式结果不对?作用域和求值时机搞清楚没
- Generator函数内部报错却捕获不到?try/catch放对位置了吗
- 用递归Generator处理嵌套结构:扁平化数组so easy
- 把普通函数"升级"成Generator:高阶函数包装术
- 和async函数互转:Generator → Promise → async 的变形记
- 别被概念吓住,Generator本质就是个能暂停的函数
小白也能搞懂的JavaScript Generator函数:异步控制流新姿势
为啥我翻遍MDN还是看不懂Generator?别慌,你不是一个人
讲真,我第一次看到MDN上Generator那堆概念的时候,整个人是懵的。什么"迭代器协议"、什么"状态机"、什么"协程"——我当时就寻思,这帮人是不是故意把简单东西说复杂,显得自己很厉害?
我就记得那天凌晨两点,项目 deadline 明天,我盯着一段 function* 的代码发呆。星号是啥?yield 又是啥?为啥这个函数跑着跑着还能停?最离谱的是,我明明 return 了一个值,它居然还能继续往下跑?这跟我学了三年 JavaScript 的认知完全对不上号啊。
后来我才想明白,Generator 这玩意儿就跟学骑自行车一样。你看一百遍理论不如摔两跤。MDN 那种写法,是给已经会骑的人看的说明书,不是给第一次摸车把的人准备的教程。
所以这篇文章,我就按我当时"摔跤"的顺序,把 Generator 掰开了揉碎了讲。不整那些高大上的术语,就聊咱们写代码时真实会遇到的场景。你要是看完还是懵,那算我输。
从"function*"说起:这星号不是装饰,是魔法开关
先说这个诡异的星号。你看这写法:
// 普通函数,大家都熟
function normalFunc() {
return 1;
}
// Generator 函数,多了个星号
function* generatorFunc() {
yield 1;
yield 2;
yield 3;
}
这个 * 放的位置就很随意,你可以 function* name(),也可以 function *name(),甚至 function * name(),JS 引擎都认。但我建议你统一写成 function*,这样一眼就能看出"这是个特殊函数"。
这个星号到底干了啥?它其实就干了一件事:把这个函数变成了一台能随时暂停的机器。
普通函数你调用,它就从第一行跑到最后一行,中间除非报错或者 return,否则不会停。但 Generator 不一样,它遇到 yield 就停,停完了还能从停的地方继续跑。这就像是看电视剧能暂停、能快进、还能倒回去重看——普通函数就是电影院,买了票就得一口气看完。
来,看个最基础的例子:
function* simpleGenerator() {
console.log('开始执行...');
yield '第一个暂停点';
console.log('恢复执行...');
yield '第二个暂停点';
console.log('最后收尾...');
return '结束了';
}
// 注意:调用 generator 函数不会立即执行,而是返回一个 Generator 对象
const gen = simpleGenerator();
// 第一次调用 next(),函数开始执行,到第一个 yield 暂停
const result1 = gen.next();
console.log(result1);
// 输出: { value: '第一个暂停点', done: false }
// 注意看,console.log('开始执行...') 这时候才打印出来
// 第二次调用 next(),从上次暂停的地方继续
const result2 = gen.next();
console.log(result2);
// 输出: { value: '第二个暂停点', done: false }
// 这时候 '恢复执行...' 才打印
// 第三次调用 next(),继续到最后
const result3 = gen.next();
console.log(result3);
// 输出: { value: '结束了', done: true }
// '最后收尾...' 打印了,而且 done 变成 true 了
// 第四次调用,已经没代码可执行了
const result4 = gen.next();
console.log(result4);
// 输出: { value: undefined, done: true }
// 永远返回 undefined,done 永远是 true
看到没?关键点来了:Generator 函数被调用时,函数体里的代码根本不执行。它返回的是一个"迭代器对象",你得手动调用 next(),它才肯动一下。而且每次 next() 的返回值都是一个固定格式的对象:{ value: xxx, done: true/false }。
这个设计就很有意思。value 是你 yield 出去的值,done 告诉你还有没有后续。这就像是跟一个很懒的朋友合作,你推一下他动一下,你不推他就躺着。但好处是,你可以完全控制节奏。
yield不是return,它更像"暂停键+传话筒"
很多人一开始会把 yield 和 return 搞混,觉得都是"交出控制权"。但这两货完全不是一回事。
return 是"老子不干了,给你个结果,函数彻底结束"。yield 是"我先歇会儿,给你个中间结果,但咱还得继续"。
而且 yield 还有个骚操作:它能双向传值。这是 return 绝对做不到的。
function* twoWayCommunication() {
// 第一次 next() 调用,执行到这里,yield 1 后暂停
// 注意:第一次 next() 不能传参,因为还没执行到 yield 呢
const received1 = yield 1;
console.log('第一次恢复,收到:', received1); // 收到: '传给yield的值'
const received2 = yield 2;
console.log('第二次恢复,收到:', received2); // 收到: 42
const received3 = yield 3;
console.log('第三次恢复,收到:', received3); // 收到: { name: '对象也行' }
}
const gen = twoWayCommunication();
// 第一次 next(),启动 Generator,运行到第一个 yield 1
// 这时候还没执行到 = yield 1 的赋值部分,所以传参没用
const step1 = gen.next();
console.log('step1:', step1); // { value: 1, done: false }
// 第二次 next(),传入的值会作为上一个 yield 表达式的结果
// 也就是说,received1 = '传给yield的值'
const step2 = gen.next('传给yield的值');
console.log('step2:', step2); // { value: 2, done: false }
// 第三次 next(),传入 42,赋值给 received2
const step3 = gen.next(42);
console.log('step3:', step3); // { value: 3, done: false }
// 第四次 next(),传入对象
const step4 = gen.next({ name: '对象也行' });
console.log('step4:', step4); // { value: undefined, done: true }
这个双向通信的机制,是 Generator 能实现复杂控制流的核心。你想啊,外部代码可以通过 next() 往 Generator 里塞数据,Generator 通过 yield 往外吐数据,这不就是完美的"生产者-消费者"模式吗?
而且 yield 后面可以跟任何表达式,甚至可以是另一个 Generator:
function* innerGenerator() {
yield '内部1';
yield '内部2';
}
function* outerGenerator() {
yield '外部开始';
// 用 yield* 委托给另一个 Generator
yield* innerGenerator();
yield '外部结束';
}
const gen = outerGenerator();
console.log([...gen]);
// ['外部开始', '内部1', '内部2', '外部结束']
yield* 这个语法也很实用,它相当于把内部 Generator 的所有 yield 都"平铺"到外部。这在处理嵌套结构或者组合多个 Generator 的时候特别方便。
next()调用就像按遥控器:播一帧、问一句、再继续
前面说了 next() 的基本用法,但这里有个细节很多人容易踩坑:next() 的传参时机。
看这段代码,你能猜出输出吗?
function* trickyExample() {
console.log('第1行');
const a = yield 'A';
console.log('第2行,a =', a);
const b = yield 'B';
console.log('第3行,b =', b);
const c = yield 'C';
console.log('第4行,c =', c);
}
const gen = trickyExample();
console.log(gen.next('第一次传参')); // 这个参数会被用到吗?
console.log(gen.next('第二次传参'));
console.log(gen.next('第三次传参'));
console.log(gen.next('第四次传参'));
答案是:第一次传参会被忽略,因为那时候 Generator 还没执行到 yield 表达式呢。具体输出是:
第1行
{ value: 'A', done: false }
第2行,a = 第二次传参
{ value: 'B', done: false }
第3行,b = 第三次传参
{ value: 'C', done: false }
第4行,c = 第四次传参
{ value: undefined, done: true }
所以规律是:第 n 次 next() 传入的参数,会成为第 n-1 个 yield 表达式的返回值。第一次 next() 只是启动 Generator,传啥都白搭。
这个设计一开始我觉得很反人类,但用多了就发现它其实很合理。你想啊,yield 表达式还没执行完,你传值给它也没地方存。只有等它 yield 出去、暂停了,下一次恢复的时候才能把值"补"进去。
普通函数 vs Generator:一个跑完就歇,一个能随时插嘴
咱们来做个对比实验,看看普通函数和 Generator 在行为上的差异:
// 普通函数:一次性执行完
function normalFunction() {
const result = [];
for (let i = 0; i < 3; i++) {
result.push(i);
console.log(`普通函数执行第${i}步`);
}
return result;
}
console.log('=== 普通函数 ===');
const normalResult = normalFunction();
console.log('结果:', normalResult);
// 输出顺序:
// 普通函数执行第0步
// 普通函数执行第1步
// 普通函数执行第2步
// 结果: [0, 1, 2]
// Generator:可以分步执行
function* generatorFunction() {
const result = [];
for (let i = 0; i < 3; i++) {
result.push(i);
console.log(`Generator执行第${i}步`);
yield i; // 每一步都暂停
}
return result;
}
console.log('\n=== Generator ===');
const gen = generatorFunction();
console.log('第一次next:', gen.next());
// Generator执行第0步
// { value: 0, done: false }
console.log('做点别的事...');
console.log('第二次next:', gen.next());
// Generator执行第1步
// { value: 1, done: false }
console.log('再摸会儿鱼...');
console.log('第三次next:', gen.next());
// Generator执行第2步
// { value: 2, done: false }
console.log('最后一次next:', gen.next());
// { value: [0, 1, 2], done: true }
看到区别了吗?普通函数一旦开始就必须跑完,Generator 可以跑一步停一步,中间还能干别的。这在处理大量数据或者需要用户交互的场景下,简直是救命稻草。
比如你要处理一个 10 万条数据的数组,普通函数直接跑可能会卡死页面,Generator 可以配合 requestAnimationFrame 分片处理,保证页面不卡顿。
function* processLargeData(data) {
const chunkSize = 1000; // 每次处理1000条
for (let i = 0; i < data.length; i += chunkSize) {
const chunk = data.slice(i, i + chunkSize);
// 处理这一批数据
const processed = chunk.map(item => item * 2); // 假设处理逻辑
yield {
progress: Math.min(i + chunkSize, data.length) / data.length,
processed: processed.length
};
}
return '全部处理完成';
}
// 使用:配合 requestAnimationFrame 不阻塞主线程
const bigData = new Array(100000).fill(0).map((_, i) => i);
const processor = processLargeData(bigData);
function processNextChunk() {
const result = processor.next();
if (!result.done) {
console.log(`处理进度: ${(result.value.progress * 100).toFixed(2)}%`);
// 让出主线程,下一帧继续
requestAnimationFrame(processNextChunk);
} else {
console.log(result.value);
}
}
processNextChunk();
这种"协程式"的编程,在 JS 单线程环境里特别有价值。你不用搞 Worker,不用拆分函数,就能实现"伪并行"。
用for…of遍历Generator:比while循环优雅一万倍
手动调用 next() 虽然灵活,但写起来啰嗦。如果你只是想遍历 Generator 的所有值,用 for...of 语法糖舒服多了:
function* numberGenerator() {
yield 1;
yield 2;
yield 3;
yield 4;
yield 5;
}
// 优雅的方式
console.log('=== for...of 遍历 ===');
for (const num of numberGenerator()) {
console.log(num); // 1, 2, 3, 4, 5
}
// 展开运算符也行
console.log('=== 展开运算符 ===');
console.log([...numberGenerator()]); // [1, 2, 3, 4, 5]
// 对比:手动 while 循环(麻烦)
console.log('=== 手动 while ===');
const gen = numberGenerator();
let result = gen.next();
while (!result.done) {
console.log(result.value);
result = gen.next();
}
但注意啊,for...of 有个坑:它不会捕获 return 的值。Generator 的 return 值在 done: true 那次 next() 里,但 for…of 遇到 done: true 就停了,不会给你那个值。
function* withReturn() {
yield 1;
yield 2;
return '我是返回值';
}
for (const val of withReturn()) {
console.log(val); // 只打印 1 和 2,'我是返回值' 丢了
}
如果你需要那个 return 值,还是得手动调用 next(),或者解构的时候注意:
const gen = withReturn();
let result;
while (!(result = gen.next()).done) {
console.log(result.value);
}
console.log('最终返回值:', result.value); // 这里能拿到 '我是返回值'
throw()和return():强行打断和体面退场的区别
除了 next(),Generator 对象还有两个方法:throw() 和 return()。这俩都是"强行干预"执行流程的,但用法不同。
throw():往 Generator 里扔个错误,如果 Generator 内部有 try…catch 就能捕获,没有就报错终止。
function* errorProneGenerator() {
try {
yield '步骤1';
yield '步骤2';
yield '步骤3';
} catch (err) {
yield `捕获到错误: ${err.message}`;
}
yield '善后工作';
}
const gen = errorProneGenerator();
console.log(gen.next()); // { value: '步骤1', done: false }
console.log(gen.next()); // { value: '步骤2', done: false }
// 突然扔个错误进去
console.log(gen.throw(new Error('出事了!')));
// { value: '捕获到错误: 出事了!', done: false }
console.log(gen.next()); // { value: '善后工作', done: false }
console.log(gen.next()); // { value: undefined, done: true }
这个机制让 Generator 可以实现错误恢复逻辑。比如做请求重试,失败了就 throw 一个错误,Generator 内部 catch 住,等几秒再试。
return():提前终止 Generator,相当于强制 return。调用后 Generator 立即进入完成状态。
function* longGenerator() {
try {
yield 1;
yield 2;
yield 3;
} finally {
// finally 块会在 return() 时执行,用于清理资源
console.log('清理资源...');
}
}
const gen = longGenerator();
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.return('提前结束')); // { value: '提前结束', done: true }
// 输出: 清理资源...
// 之后再调用 next() 也没用了
console.log(gen.next()); // { value: undefined, done: true }
return() 常用于提前终止遍历,比如在 for…of 里用了 break,底层其实就是调用了 return()。
function* resourceHeavyGenerator() {
try {
console.log('打开数据库连接');
yield '数据1';
yield '数据2';
yield '数据3';
} finally {
console.log('关闭数据库连接'); // 保证清理
}
}
// 用了 break,finally 会执行
for (const data of resourceHeavyGenerator()) {
console.log(data);
if (data === '数据1') break; // 这里会触发 return()
}
// 输出:
// 打开数据库连接
// 数据1
// 关闭数据库连接
这个 finally 机制很重要,它让 Generator 可以安全地管理资源,不像普通函数 break 了就啥也不管了。
同步代码写异步逻辑:不用async/await也能装大神
好了,前面都是基础,现在进入 Generator 最装逼的领域:用同步写法处理异步操作。
在 async/await 出现之前(ES2017),Generator + Promise 是实现"伪同步"异步编程的主流方案。Redux-Saga、co 库这些都是基于这个原理。
核心思想是:Generator yield 出一个 Promise,然后外部的执行器(runner)等 Promise resolve 了,再把结果通过 next() 传回去。
来看个简单的实现:
// 模拟异步操作
function fetchUserData(userId) {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ id: userId, name: '张三', age: 25 });
}, 1000);
});
}
function fetchOrders(userId) {
return new Promise((resolve) => {
setTimeout(() => {
resolve([
{ id: 1, total: 100 },
{ id: 2, total: 200 }
]);
}, 800);
});
}
// Generator 里写起来像同步代码
function* loadUserFlow() {
try {
console.log('开始加载用户...');
const user = yield fetchUserData(1); // 看起来是同步的,实际是异步等待
console.log('用户加载完成:', user);
console.log('开始加载订单...');
const orders = yield fetchOrders(user.id);
console.log('订单加载完成:', orders);
return { user, orders };
} catch (error) {
console.error('出错了:', error);
throw error;
}
}
// 执行器:负责处理 Generator 和 Promise 的交互
function runGenerator(gen) {
const iterator = gen();
function handle(result) {
if (result.done) {
return Promise.resolve(result.value);
}
// result.value 应该是 Promise
return Promise.resolve(result.value)
.then(
res => handle(iterator.next(res)), // 成功,传回结果
err => handle(iterator.throw(err)) // 失败,抛出错误
);
}
return handle(iterator.next());
}
// 使用
runGenerator(loadUserFlow)
.then(data => console.log('最终结果:', data))
.catch(err => console.error('流程失败:', err));
// 输出顺序:
// 开始加载用户...
// (等1秒)
// 用户加载完成: { id: 1, name: '张三', age: 25 }
// 开始加载订单...
// (等0.8秒)
// 订单加载完成: [ { id: 1, total: 100 }, { id: 2, total: 200 } ]
// 最终结果: { user: {...}, orders: [...] }
看到没?在 Generator 函数内部,你完全感觉不到异步的回调嵌套,就像写同步代码一样清爽。这就是 Generator 在异步领域的威力。
当然,现在有了 async/await,我们不需要自己写 runGenerator 了。但理解这个原理很重要,因为很多底层库还是这么实现的,而且面试也爱考。
懒加载数据流:内存不爆、性能不崩的秘密武器
Generator 另一个杀手级特性是惰性求值(Lazy Evaluation)。它不会一次性生成所有数据,而是按需生产,这在处理大数据流时简直是救命稻草。
想象你要处理一个无限序列,比如斐波那契数列:
// 普通数组?不可能,无限内存都不够
// const fibArray = [1, 1, 2, 3, 5, 8, ...]; // 死定了
// Generator:要多少取多少
function* fibonacci() {
let [prev, curr] = [0, 1];
while (true) { // 无限循环!但不会爆栈
yield curr;
[prev, curr] = [curr, prev + curr];
}
}
const fib = fibonacci();
// 取前10个
console.log('前10个斐波那契数:');
for (let i = 0; i < 10; i++) {
console.log(fib.next().value);
}
// 1, 1, 2, 3, 5, 8, 13, 21, 34, 55
// 再取5个,继续之前的序列
console.log('再取5个:');
for (let i = 0; i < 5; i++) {
console.log(fib.next().value);
}
// 89, 144, 233, 377, 610
这个无限序列用普通函数绝对实现不了,但 Generator 可以,因为它不会真的无限计算,只是准备好了"随时可以计算"的状态。
实际应用场景:处理大文件读取。比如你要读一个 10GB 的日志文件,不可能一次性读进内存:
// 模拟逐行读取大文件
function* readLines(fileContent) {
// 假设 fileContent 是巨大字符串,我们按行分割
const lines = fileContent.split('\n');
for (const line of lines) {
yield line; // 一次只 yield 一行
}
}
// 处理日志:找出包含 ERROR 的行,但只找前100条
function* filterErrors(lineGenerator) {
let count = 0;
for (const line of lineGenerator) {
if (line.includes('ERROR')) {
yield line;
count++;
if (count >= 100) break; // 找到100条就停,后面的不读了
}
}
}
// 使用
const hugeLog = 'INFO: xxx\nERROR: 数据库连接失败\nINFO: yyy\nERROR: 请求超时\n...'; // 假设这是10GB数据
const errorLines = filterErrors(readLines(hugeLog));
for (const error of errorLines) {
console.log('发现错误:', error);
}
// 只处理了前100条 ERROR 就停止,不会把整个文件读进内存
这种流水线式的处理,每个环节都是惰性求值,内存占用始终只有当前处理的那一行。这就是 Generator 在函数式编程里的核心价值。
状态机实现神器:复杂流程控制不再靠if-else堆成山
写业务代码最头疼的是什么?我觉得是复杂的状态流转。比如一个订单系统:待支付 -> 已支付 -> 已发货 -> 已签收 -> 已完成。每个状态能执行的操作还不一样,用 if-else 写就是灾难。
Generator 天生就是状态机,因为它能记住执行位置。看这段代码:
// 订单状态机
function* orderStateMachine(order) {
let state = 'PENDING_PAYMENT';
let result;
while (true) {
switch (state) {
case 'PENDING_PAYMENT':
result = yield {
state,
actions: ['pay', 'cancel'],
data: order
};
if (result.action === 'pay') {
state = 'PAID';
order.paidAt = new Date();
} else if (result.action === 'cancel') {
state = 'CANCELLED';
}
break;
case 'PAID':
result = yield {
state,
actions: ['ship', 'refund'],
data: order
};
if (result.action === 'ship') {
state = 'SHIPPED';
order.shippedAt = new Date();
} else if (result.action === 'refund') {
state = 'REFUNDING';
}
break;
case 'SHIPPED':
result = yield {
state,
actions: ['confirm', 'queryLogistics'],
data: order
};
if (result.action === 'confirm') {
state = 'COMPLETED';
order.completedAt = new Date();
return order; // 流程结束
}
break;
case 'CANCELLED':
case 'REFUNDING':
case 'COMPLETED':
yield { state, actions: [], data: order, final: true };
return; // 终态
default:
throw new Error(`未知状态: ${state}`);
}
}
}
// 使用状态机
const order = { id: '123', amount: 99 };
const machine = orderStateMachine(order);
// 获取当前状态和可用操作
let step = machine.next();
console.log(step.value);
// { state: 'PENDING_PAYMENT', actions: ['pay', 'cancel'], data: {...} }
// 执行支付操作
step = machine.next({ action: 'pay' });
console.log(step.value.state); // 'PAID'
// 执行发货操作
step = machine.next({ action: 'ship' });
console.log(step.value.state); // 'SHIPPED'
// 执行确认收货
step = machine.next({ action: 'confirm' });
console.log(step.value.state); // 'COMPLETED'
console.log(step.done); // true
这种写法的好处是:
- 状态流转可视化:代码结构就是状态图
- 非法操作天然防呆:当前状态不支持的操作,你根本传不进去(因为 state machine 只接受特定 action)
- 历史状态可追踪:因为是在 Generator 里,你可以随时加日志、做校验
对比传统的 if-else 地狱:
// 传统写法,维护起来想死
function handleOrderAction(order, action) {
if (order.status === 'PENDING_PAYMENT') {
if (action === 'pay') {
order.status = 'PAID';
} else if (action === 'cancel') {
order.status = 'CANCELLED';
} else {
throw new Error('非法操作');
}
} else if (order.status === 'PAID') {
if (action === 'ship') {
order.status = 'SHIPPED';
} else if (action === 'refund') {
order.status = 'REFUNDING';
}
// ... 还要检查是不是从 PENDING_PAYMENT 来的,防止跳过步骤
}
// ... 还有十几个状态
}
Generator 版本的优势随着状态复杂度增加而指数级增长。而且你还可以给状态机加"钩子":
function* orderStateMachineWithHooks(order, hooks) {
let state = 'PENDING_PAYMENT';
const transition = (from, to, action) => {
if (hooks.onBeforeTransition) {
hooks.onBeforeTransition({ from, to, action });
}
state = to;
if (hooks.onAfterTransition) {
hooks.onAfterTransition({ from, to, action });
}
};
// ... 状态逻辑,用 transition 代替直接赋值
}
浏览器兼容性翻车现场:IE?别提了,提就是泪
说了这么多好处,该泼冷水了。Generator 是 ES6 特性,IE 全系不支持,包括 IE11。如果你还要兼容 IE,要么用 Babel 转译(会生成很丑的 state machine 代码),要么就别用。
现代浏览器(Chrome、Firefox、Safari、Edge)都支持得很好,但老项目迁移要注意。而且 Generator 转译后的代码调试起来很痛苦,sourcemap 有时候对不上。
// 你写的代码
function* myGen() {
yield 1;
yield 2;
}
// Babel 转译后(大概长这样,更复杂)
var _marked = regeneratorRuntime.mark(myGen);
function myGen() {
return regeneratorRuntime.wrap(function myGen$(_context) {
while (1) {
switch (_context.prev = _context.next) {
case 0:
_context.next = 2;
return 1;
case 2:
_context.next = 4;
return 2;
case 4:
case "end":
return _context.stop();
}
}
}, _marked);
}
看到没?一个 yield 变成了 switch case 的 state machine。虽然功能一样,但调试的时候你看到的是转译后的代码,不是你写的源码,断点打得你想哭。
调试体验差到想砸键盘:断点打哪?call stack看懵
这是 Generator 最大的槽点之一。你在 Chrome DevTools 里调试 Generator,经常会遇到:
-
Call Stack 看不懂:因为 Generator 是"暂停-恢复"机制,调用栈不是传统的函数嵌套,而是同一个函数在不同时间点执行。你看到的栈信息可能是乱的。
-
变量作用域诡异:yield 前后的变量,在 DevTools 的 Scope 面板里显示方式很奇怪,有时候你明明赋值了,却显示 undefined,因为执行上下文还没创建完。
-
Step Over 行为异常:你想单步跳过,结果直接跳到了下一个 yield,中间的代码好像没执行?其实是执行了,只是 DevTools 的显示有延迟。
我的建议是:调试 Generator 的时候,多用 console.log,少用断点。或者把逻辑拆成小函数,别让一个 Generator 函数太长。
和Promise混用容易把自己绕进去:谁先谁后?谁resolve谁?
Generator 和 Promise 混用的时候,有几个常见坑:
坑1:忘记 yield
function* badExample() {
// 错误:忘了 yield,fetch 返回的是 Promise,不是结果
const data = fetch('/api/user');
console.log(data); // 输出 Promise 对象,不是 user 数据
}
// 正确写法
function* goodExample() {
const data = yield fetch('/api/user'); // yield 出去,等执行器处理
console.log(data); // 这才是 resolve 后的数据
}
坑2:yield 了非 Promise
如果你用的执行器(比如 co)期望 yield 的都是 Promise,但你 yield 了个普通值,有些执行器会报错,有些会当成已 resolve 的 Promise 处理。行为不一致,容易出 bug。
坑3:并行执行
Generator 是顺序执行的,如果你想并行发多个请求,得用特殊技巧:
function* parallelRequests() {
// 错误:顺序执行,慢
const user = yield fetchUser();
const orders = yield fetchOrders(); // 等 fetchUser 完了才执行
// 正确:同时发起,用 Promise.all
const [user, orders] = yield Promise.all([
fetchUser(),
fetchOrders()
]);
}
坑4:错误处理边界
Generator 内部的 try…catch 能捕获 yield 出去的 Promise 的 reject 吗?取决于你的执行器实现。标准的 co 库会帮你做,但自己写的执行器可能漏掉。
function* errorHandling() {
try {
// 如果执行器正确处理,这里能捕获
const data = yield Promise.reject(new Error('挂了'));
} catch (err) {
console.log('捕获到:', err.message);
}
}
用Generator做请求重试机制:失败3次再放弃,稳得很
实际项目里,Generator 最适合做这种"有固定流程、需要状态记忆"的逻辑。比如请求重试:
function* retryFetch(url, options = {}, maxRetries = 3) {
let lastError;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
console.log(`第${attempt}次尝试...`);
const response = yield fetch(url, options);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return yield response.json(); // 成功,返回数据
} catch (error) {
lastError = error;
console.log(`第${attempt}次失败: ${error.message}`);
if (attempt < maxRetries) {
// 指数退避:等 1s, 2s, 4s...
const delay = Math.pow(2, attempt - 1) * 1000;
console.log(`等待${delay}ms后重试...`);
yield new Promise(resolve => setTimeout(resolve, delay));
}
}
}
throw new Error(`${maxRetries}次尝试均失败: ${lastError.message}`);
}
// 执行器
function runWithRetry(genFunc) {
const iterator = genFunc();
function step(result) {
if (result.done) return Promise.resolve(result.value);
return Promise.resolve(result.value)
.then(
res => step(iterator.next(res)),
err => step(iterator.throw(err))
);
}
return step(iterator.next());
}
// 使用:模拟一个会失败两次的接口
let failCount = 0;
const mockFetch = () => new Promise((resolve, reject) => {
failCount++;
if (failCount <= 2) {
reject(new Error('网络抖动'));
} else {
resolve({ ok: true, json: () => Promise.resolve({ data: 'success' }) });
}
});
runWithRetry(function* () {
return yield retryFetch('https://api.example.com/data', {}, 3);
})
.then(data => console.log('最终成功:', data))
.catch(err => console.error('彻底失败:', err));
// 输出:
// 第1次尝试...
// 第1次失败: 网络抖动
// 等待1000ms后重试...
// 第2次尝试...
// 第2次失败: 网络抖动
// 等待2000ms后重试...
// 第3次尝试...
// 最终成功: { data: 'success' }
这个实现的好处是:重试逻辑和业务逻辑完全分离,而且支持"取消"——如果你不想重试了,直接不调用 next() 就行,或者调用 return() 终止。
模拟无限滚动列表:滑到底自动yield新数据
前端最常见的交互之一,用 Generator 实现特别优雅:
// 模拟后端分页接口
function fetchPage(pageNum, pageSize = 20) {
return new Promise(resolve => {
setTimeout(() => {
const items = Array.from({ length: pageSize }, (_, i) => ({
id: (pageNum - 1) * pageSize + i + 1,
title: `文章标题 ${(pageNum - 1) * pageSize + i + 1}`,
content: '内容略...'
}));
resolve({
data: items,
hasMore: pageNum < 5 // 假设只有5页
});
}, 300);
});
}
// Generator 管理分页状态
function* infiniteScrollLoader() {
let pageNum = 1;
const pageSize = 20;
let isLoading = false;
let hasMore = true;
while (hasMore) {
if (isLoading) {
yield { type: 'loading', message: '正在加载...' };
continue;
}
isLoading = true;
try {
const response = yield fetchPage(pageNum, pageSize);
hasMore = response.hasMore;
pageNum++;
isLoading = false;
yield {
type: 'data',
data: response.data,
hasMore,
page: pageNum - 1
};
} catch (error) {
isLoading = false;
yield { type: 'error', error: error.message };
// 出错后可以选择重试或停止
yield { type: 'retry_prompt' };
}
}
yield { type: 'complete', message: '没有更多数据了' };
}
// 在 React 组件中使用(伪代码)
function ArticleList() {
const [articles, setArticles] = useState([]);
const [hasMore, setHasMore] = useState(true);
const [loading, setLoading] = useState(false);
// 用 ref 存 generator,保证跨渲染周期不丢失状态
const loaderRef = useRef(null);
useEffect(() => {
loaderRef.current = infiniteScrollLoader();
loadMore(); // 加载第一页
}, []);
const loadMore = async () => {
if (!loaderRef.current || loading) return;
const { value, done } = loaderRef.current.next();
if (done) return;
if (value.type === 'loading') {
setLoading(true);
// 实际应该等 fetchPage 的 Promise resolve,这里简化
} else if (value.type === 'data') {
setArticles(prev => [...prev, ...value.data]);
setHasMore(value.hasMore);
setLoading(false);
} else if (value.type === 'error') {
setLoading(false);
// 处理错误...
}
};
return (
<div>
{articles.map(article => (
<ArticleCard key={article.id} {...article} />
))}
{hasMore && <div onClick={loadMore}>加载更多</div>}
</div>
);
}
Generator 在这里的作用是封装分页状态。你不需要在组件里维护 pageNum、hasMore 这些变量,Generator 自己记得当前读到第几页。组件只负责触发和展示,逻辑清晰很多。
配合Redux-Saga管理副作用:虽然现在没人用了但面试还考
Redux-Saga 是 Generator 在 React 生态里的高光时刻。虽然现在 TanStack Query、SWR 这些更流行,但理解 Saga 对面试和读老代码很有帮助。
核心概念:Saga 是"长期存活的 Generator",它监听 action,然后执行副作用(异步操作)。
// 一个典型的 Saga
import { take, put, call, fork, cancel } from 'redux-saga/effects';
// 监听登录请求
function* loginFlow() {
while (true) {
// 等待 LOGIN_REQUEST action
const { payload } = yield take('LOGIN_REQUEST');
// fork 一个非阻塞任务
const task = yield fork(authorize, payload.username, payload.password);
// 同时监听 LOGOUT 或 LOGIN_ERROR
const action = yield take(['LOGOUT', 'LOGIN_ERROR']);
if (action.type === 'LOGOUT') {
yield cancel(task); // 取消登录任务
yield call(clearToken); // 清理 token
}
}
}
// 实际的授权逻辑
function* authorize(username, password) {
try {
const token = yield call(api.login, username, password);
yield put({ type: 'LOGIN_SUCCESS', token });
yield call(storeToken, token);
// 登录成功后,可以启动其他 saga
yield fork(userSaga);
} catch (error) {
yield put({ type: 'LOGIN_ERROR', error: error.message });
}
}
这里的 take、put、call、fork、cancel 都是"Effect",它们被 yield 出去后,由 Redux-Saga 的中间件解释执行。Generator 本身只是描述"我要做什么",实际执行由外部控制。
这种模式的优势:
- 可测试性:你可以不启动 saga,直接检查 yield 了哪些 effect
- 可组合性:saga 可以互相调用,像函数一样
- 可取消性:通过 cancel 可以终止正在进行的异步操作
虽然现在用的人少了,但这种"用 Generator 描述副作用"的思想,在现代的 React Server Components、Remix 的 loader/action 里还能看到影子。
为啥next()返回值是{ value: xxx, done: false }?结构拆解讲透
前面一直用这个结构,现在详细说说设计原因。
interface IteratorResult<T> {
value: T; // 当前步骤产出的值
done: boolean; // 迭代器是否已完成
}
这个结构是 Iterator Protocol 的标准,所有可迭代对象(Array、Map、Set、Generator)都遵循。它解决了几个关键问题:
问题1:如何区分"值是 undefined"和"迭代结束"
如果没有 done 标志,单纯返回 undefined,你无法知道是"yield 了 undefined"还是"迭代完了"。
function* yieldsUndefined() {
yield 1;
yield undefined; // 故意 yield undefined
yield 2;
}
const gen = yieldsUndefined();
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: undefined, done: false } - 看,done 是 false!
console.log(gen.next()); // { value: 2, done: false }
console.log(gen.next()); // { value: undefined, done: true } - 这次 done 是 true
问题2:如何获取 return 的值
return 的值在最后一次 next() 里,通过 value 字段携带,done 标记为 true。
function* withReturnValue() {
yield 1;
return '我是返回值';
}
const gen = withReturnValue();
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: '我是返回值', done: true }
问题3:for…of 如何知道何时停止
for…of 底层就是不断调用 next(),直到遇到 done: true 就停。这也是为什么它拿不到 return 值——它看到 done: true 就退出了,不 care value 是啥。
这个结构虽然看起来啰嗦,但它是整个迭代器生态的基础。理解它,你就理解了 JS 中所有遍历行为的底层逻辑。
yield后面跟表达式结果不对?作用域和求值时机搞清楚没
有个细节很多人栽过跟头:yield 的优先级和求值时机。
function* precedenceTrap() {
const a = 1;
const b = 2;
// 你以为 yield a + b 是 yield (a + b),其实是 (yield a) + b
const result = yield a + b;
console.log(result); // 猜猜是多少?
}
const gen = precedenceTrap();
console.log(gen.next()); // { value: 3, done: false }
console.log(gen.next(10)); // 传入10,result 是多少?
答案是 result 是 NaN。因为 yield a + b 被解析为 (yield a) + b,先 yield 了 a(值为1),然后试图 1 + b,但这时候还没恢复执行呢。等 next(10) 传进来,其实是赋值给 yield a 这个表达式,所以 result = 10 + 2 = 12?不对,我刚才说错了,重新看:
实际上 yield a + b 的优先级是 yield (a + b),因为 yield 的优先级很低,比 + 还低。所以第一次 next() 返回 3。然后 next(10) 传入的 10 成为 yield (a + b) 的结果,所以 result 是 10。
我之前说错了,验证一下:
function* test() {
const r = yield 1 + 2;
console.log('r:', r);
}
const g = test();
console.log(g.next()); // { value: 3, done: false }
g.next(100); // r: 100
对,第一次 yield 出去的是 3,第二次传入的 100 被赋值给 r。所以 yield 优先级确实比 + 低,整个表达式先求值再 yield。
但如果你想 yield 一个表达式结果,最好用括号明确:
yield (a + b); // 明确先算加法
yield a || b; // yield 的优先级比 || 还低,这里会 yield a,如果 a 是 falsy 才 yield b?不对,实际上...
查一下优先级表:yield < 赋值运算符 < 逗号运算符。所以 yield a, b 是 (yield a), b,不是 yield (a, b)。
这种细节在写复杂表达式的时候容易翻车,建议 yield 后面要么简单变量,要么明确加括号。
Generator函数内部报错却捕获不到?try/catch放对位置了吗
Generator 的错误处理有两个层面:Generator 内部捕获,和外部执行器捕获。
情况1:内部 yield 的 Promise reject
function* internalCatch() {
try {
const data = yield Promise.reject(new Error('内部错误'));
} catch (err) {
console.log('内部捕获:', err.message); // 能捕获吗?
yield '错误已处理';
}
}
// 如果用普通的 runGenerator,需要正确处理 throw
function runWithErrorHandling(gen) {
const iterator = gen();
function step(result) {
if (result.done) return Promise.resolve(result.value);
return Promise.resolve(result.value)
.then(
res => step(iterator.next(res)),
err => {
// 关键:把 reject 转换成 Generator 的 throw
try {
return step(iterator.throw(err));
} catch (genErr) {
// Generator 内部没捕获,这里会抛出
return Promise.reject(genErr);
}
}
);
}
return step(iterator.next());
}
runWithErrorHandling(internalCatch)
.then(val => console.log('成功:', val))
.catch(err => console.log('外部捕获:', err.message));
如果执行器正确地用 iterator.throw(err),那么 Generator 内部的 try…catch 就能捕获到。如果执行器只是 reject 了 Promise,没调用 throw,那 Generator 内部是感知不到的。
情况2:Generator 内部同步错误
function* syncError() {
yield 1;
throw new Error('同步错误');
yield 2; // 不会执行
}
const gen = syncError();
console.log(gen.next()); // { value: 1, done: false }
try {
console.log(gen.next()); // 这里抛出 Error: 同步错误
} catch (err) {
console.log('外部捕获:', err.message);
}
// 之后再调用 next()
console.log(gen.next()); // { value: undefined, done: true }
// 一旦抛出错误,Generator 就进入了终止状态,之后再调用都是 undefined
注意:Generator 内部 throw 错误后,这个 Generator 就废了,不能再恢复。这和 Promise 不一样,Promise reject 了还能链式调用 catch。
情况3:外部通过 throw() 方法注入错误
function* externalError() {
try {
yield 1;
yield 2; // 如果外部 throw,这里不会执行
} catch (err) {
yield `捕获外部错误: ${err.message}`;
}
yield 3;
}
const gen = externalError();
console.log(gen.next()); // { value: 1, done: false }
// 外部注入错误
console.log(gen.throw(new Error('外部来的')));
// { value: '捕获外部错误: 外部来的', done: false }
console.log(gen.next()); // { value: 3, done: false }
这个机制让 Generator 可以实现"取消"语义——外部不想等了,就 throw 一个 CancelError,Generator 内部 catch 住做清理。
用递归Generator处理嵌套结构:扁平化数组so easy
处理树形结构或者嵌套数组,递归 Generator 很优雅:
// 递归遍历树形结构
function* traverseTree(node) {
yield node.value;
if (node.children) {
for (const child of node.children) {
yield* traverseTree(child); // 用 yield* 委托递归
}
}
}
const tree = {
value: 1,
children: [
{ value: 2, children: [{ value: 4 }, { value: 5 }] },
{ value: 3, children: [{ value: 6 }] }
]
};
console.log([...traverseTree(tree)]); // [1, 2, 4, 5, 3, 6]
// 对比:普通递归函数需要维护数组
function traverseTreeNormal(node) {
const result = [node.value];
if (node.children) {
for (const child of node.children) {
result.push(...traverseTreeNormal(child));
}
}
return result;
}
// 递归深的时候可能爆栈,而且内存占用高
无限递归的安全版本:
// 用 Generator 实现深度优先搜索,不会爆栈
function* dfs(graph, startNode) {
const visited = new Set();
const stack = [startNode];
while (stack.length > 0) {
const node = stack.pop();
if (!visited.has(node)) {
visited.add(node);
yield node;
// 把邻居压栈,下次迭代处理
const neighbors = graph[node] || [];
for (const neighbor of neighbors) {
if (!visited.has(neighbor)) {
stack.push(neighbor);
}
}
}
}
}
// 使用
const graph = {
A: ['B', 'C'],
B: ['D', 'E'],
C: ['F'],
D: [],
E: ['F'],
F: []
};
console.log('DFS 遍历:');
for (const node of dfs(graph, 'A')) {
console.log(node); // A, C, F, B, E, D(取决于压栈顺序)
}
这种写法的好处是:你可以随时暂停遍历,去做别的事,然后再继续。普通递归函数一旦开始就必须走完。
把普通函数"升级"成Generator:高阶函数包装术
有时候你想把现有的普通函数改造成 Generator,可以用高阶函数包装:
// 高阶函数:把任意函数转成 Generator
function toGenerator(fn) {
return function* (...args) {
yield 'start'; // 可以插入生命周期钩子
const result = fn(...args);
yield 'end';
return result;
};
}
const syncAdd = (a, b) => a + b;
const genAdd = toGenerator(syncAdd);
const gen = genAdd(1, 2);
console.log(gen.next()); // { value: 'start', done: false }
console.log(gen.next()); // { value: 'end', done: false }
console.log(gen.next()); // { value: 3, done: true }
// 更实用的:带日志和计时的包装
function withLogging(fn) {
return function* (...args) {
console.log(`[${fn.name}] 开始执行,参数:`, args);
const start = Date.now();
try {
const result = fn(...args);
const duration = Date.now() - start;
console.log(`[${fn.name}] 执行成功,耗时${duration}ms`);
yield { status: 'success', duration, result };
return result;
} catch (error) {
const duration = Date.now() - start;
console.log(`[${fn.name}] 执行失败,耗时${duration}ms,错误:`, error.message);
yield { status: 'error', duration, error };
throw error;
}
};
}
const loggedFetch = withLogging(fetch);
// 现在 loggedFetch 返回 Generator,可以逐步执行并观察状态
这种包装模式在测试和调试时很有用,你可以"拦截"函数的执行过程,插入观察点。
和async函数互转:Generator → Promise → async 的变形记
最后聊聊 Generator 和 async/await 的关系。其实 async/await 就是 Generator + Promise 的语法糖。理解这个转换,对你理解底层很有帮助。
Generator 转 async:
// Generator 版本
function* oldStyle() {
const user = yield fetchUser();
const orders = yield fetchOrders(user.id);
return { user, orders };
}
// 手动转 async(实际上就是 babel 转译 async 函数的方式)
function newStyle() {
return _asyncToGenerator(function* () {
const user = yield fetchUser();
const orders = yield fetchOrders(user.id);
return { user, orders };
})();
}
// _asyncToGenerator 大概长这样:
function _asyncToGenerator(genFn) {
return function () {
const gen = genFn.apply(this, arguments);
return new Promise((resolve, reject) => {
function step(key, arg) {
let info;
try {
info = gen[key](arg); // key 是 'next' 或 'throw'
} catch (error) {
reject(error);
return;
}
if (info.done) {
resolve(info.value);
} else {
Promise.resolve(info.value).then(
value => step('next', value),
err => step('throw', err)
);
}
}
step('next');
});
};
}
看到没?async 函数本质上就是一个自动执行 Generator 并返回 Promise 的包装器。这也是为什么 async/await 能处理异步——底层还是 Generator 那一套。
async 转 Generator:
反过来,如果你想把 async 函数改成 Generator(比如为了更细粒度的控制),可以这样做:
// async 版本
async function fetchData() {
const res = await fetch('/api/data');
const json = await res.json();
return json;
}
// Generator 版本(需要外部执行器)
function* fetchDataGen() {
const res = yield fetch('/api/data');
const json = yield res.json();
return json;
}
// 使用 co 或类似库执行
const co = require('co');
co(fetchDataGen).then(data => console.log(data));
虽然看起来 Generator 版本更啰嗦,但它给了你"暂停"和"干预"的能力。async 函数一旦开始就必须跑完,Generator 可以随时中断。
别被概念吓住,Generator本质就是个能暂停的函数
写到这儿,我觉得该说的都说了。最后总结几句掏心窝子的话。
Generator 这玩意儿,概念上确实有点绕。什么"迭代器协议"、“协程”、“状态机”,听着挺唬人。但你把它当成一个能记住执行位置的函数,就简单多了。
普通函数:调用 → 执行 → 返回 → 销毁,一次性用品。
Generator:调用 → 创建 → 按需执行 → 暂停 → 恢复 → … → 销毁,可重复利用。
这个"暂停-恢复"的能力,在 JS 这种单线程语言里特别珍贵。你不用搞多线程,不用写回调地狱,就能实现复杂的流程控制。
当然,现在 async/await 确实更香,大部分场景不需要手动折腾 Generator。但理解 Generator 能让你:
- 看懂老代码:Redux-Saga、co、Koa 1.x 这些还在维护的项目
- 面试装X:“你知道 async/await 底层是怎么实现的吗?”
- 处理特殊场景:需要"可中断迭代"、“惰性求值”、"状态机"的时候,Generator 依然是最佳选择
下次同事吹牛说"我用过协程",你就微微一笑:“哦,你说 Generator 啊?那玩意儿我天天用,不就是 function* 加个 yield 嘛,还能 throw 和 return,配合 Promise 能实现异步流程控制,不过现在有 async/await 了,但理解原理对调试很有帮助…”
然后看着他懵逼的表情,深藏功与名。
反正我当年要是有人这么给我讲,能少熬好几个通宵。现在这文章写给你,算是积德了。拿去用,不用谢,请叫我雷锋。

更多推荐



所有评论(0)