JavaScript Generator 函数详解:异步控制流与状态机应用
为啥我翻遍 MDN 还是看不懂 Generator?别慌,你不是一个人
讲真,我第一次看到 MDN 上 Generator 那堆概念的时候,整个人是懵的。什么"迭代器协议"、什么"状态机"、什么"协程"——我当时就寻思,这帮人是不是故意把简单东西说复杂,显得自己很厉害?
我就记得那天凌晨两点,项目 deadline 明天,我盯着一段 function* 的代码发呆。星号是啥?yield 又是啥?为啥这个函数跑着跑着还能停?最离谱的是,我明明 return 了一个值,它居然还能继续往下跑?这跟我学了三年 JavaScript 的认知完全对不上号啊。
后来我才想明白,Generator 这玩意儿就跟学骑自行车一样。你看一百遍理论不如摔两跤。MDN 那种写法,是给已经会骑的人看的说明书,不是给第一次摸车把的人准备的教程。
所以这篇文章,我就按我当时"摔跤"的顺序,把 Generator 掰开了揉碎了讲。不整那些高大上的术语,就聊咱们写代码时真实会遇到的场景。你要是看完还是懵,那算我输。
从"function*"说起:这星号不是装饰,是魔法开关
先说这个诡异的星号。你看这写法:
function normalFunc() {
return 1;
}
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 '结束了';
}
const gen = simpleGenerator();
const result1 = gen.next();
console.log(result1);
const result2 = gen.next();
console.log(result2);
const result3 = gen.next();
console.log(result3);
const result4 = gen.next();
console.log(result4);
看到没?关键点来了:Generator 函数被调用时,函数体里的代码根本不执行。它返回的是一个"迭代器对象",你得手动调用 next(),它才肯动一下。而且每次 next() 的返回值都是一个固定格式的对象:{ value: xxx, done: true/false }。
这个设计就很有意思。value 是你 yield 出去的值,done 告诉你还有没有后续。这就像是跟一个很懒的朋友合作,你推一下他动一下,你不推他就躺着。但好处是,你可以完全控制节奏。
yield 不是 return,它更像"暂停键 + 传话筒"
很多人一开始会把 yield 和 return 搞混,觉得都是"交出控制权"。但这两货完全不是一回事。
return 是"老子不干了,给你个结果,函数彻底结束"。yield 是"我先歇会儿,给你个中间结果,但咱还得继续"。
而且 yield 还有个骚操作:它能双向传值。这是 return 绝对做不到的。
function* twoWayCommunication() {
const received1 = yield 1;
console.log('第一次恢复,收到:', received1);
const received2 = yield 2;
console.log('第二次恢复,收到:', received2);
const received3 = yield 3;
console.log('第三次恢复,收到:', received3);
}
const gen = twoWayCommunication();
const step1 = gen.next();
console.log('step1:', step1);
const step2 = gen.next('传给 yield 的值');
console.log('step2:', step2);
const step3 = gen.next(42);
.(, step3);
step4 = gen.({ : });
.(, step4);
这个双向通信的机制,是 Generator 能实现复杂控制流的核心。你想啊,外部代码可以通过 next() 往 Generator 里塞数据,Generator 通过 yield 往外吐数据,这不就是完美的"生产者 - 消费者"模式吗?
而且 yield 后面可以跟任何表达式,甚至可以是另一个 Generator:
function* innerGenerator() {
yield '内部 1';
yield '内部 2';
}
function* outerGenerator() {
yield '外部开始';
yield* innerGenerator();
yield '外部结束';
}
const gen = outerGenerator();
console.log([...gen]);
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);
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.());
.();
.(, gen.());
.();
.(, gen.());
.(, gen.());
看到区别了吗?普通函数一旦开始就必须跑完,Generator 可以跑一步停一步,中间还能干别的。这在处理大量数据或者需要用户交互的场景下,简直是救命稻草。
比如你要处理一个 10 万条数据的数组,普通函数直接跑可能会卡死页面,Generator 可以配合 requestAnimationFrame 分片处理,保证页面不卡顿。
function* processLargeData(data) {
const chunkSize = 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 '全部处理完成';
}
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()}%`);
(processNextChunk);
} {
.(result.);
}
}
();
这种"协程式"的编程,在 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);
}
console.log('=== 展开运算符 ===');
console.log([...numberGenerator()]);
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);
}
如果你需要那个 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());
console.log(gen.next());
console.log(gen.throw(new Error('出事了!')));
console.log(gen.next());
console.log(gen.next());
这个机制让 Generator 可以实现错误恢复逻辑。比如做请求重试,失败了就 throw 一个错误,Generator 内部 catch 住,等几秒再试。
return():提前终止 Generator,相当于强制 return。调用后 Generator 立即进入完成状态。
function* longGenerator() {
try {
yield 1;
yield 2;
yield 3;
} finally {
console.log('清理资源...');
}
}
const gen = longGenerator();
console.log(gen.next());
console.log(gen.return('提前结束'));
console.log(gen.next());
return() 常用于提前终止遍历,比如在 for…of 里用了 break,底层其实就是调用了 return()。
function* resourceHeavyGenerator() {
try {
console.log('打开数据库连接');
yield '数据 1';
yield '数据 2';
yield '数据 3';
} finally {
console.log('关闭数据库连接');
}
}
for (const data of resourceHeavyGenerator()) {
console.log(data);
if (data === '数据 1') break;
}
这个 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);
});
}
function* loadUserFlow() {
try {
console.log('开始加载用户...');
const user = yield fetchUserData(1);
console.log('用户加载完成:', user);
console.();
orders = (user.);
.(, orders);
{ user, orders };
} (error) {
.(, error);
error;
}
}
() {
iterator = ();
() {
(result.) {
.(result.);
}
.(result.).(
(iterator.(res)),
(iterator.(err))
);
}
(iterator.());
}
(loadUserFlow)
.( .(, data))
.( .(, err));
看到没?在 Generator 函数内部,你完全感觉不到异步的回调嵌套,就像写同步代码一样清爽。这就是 Generator 在异步领域的威力。
当然,现在有了 async/await,我们不需要自己写 runGenerator 了。但理解这个原理很重要,因为很多底层库还是这么实现的,而且面试也爱考。
懒加载数据流:内存不爆、性能不崩的秘密武器
Generator 另一个杀手级特性是惰性求值(Lazy Evaluation)。它不会一次性生成所有数据,而是按需生产,这在处理大数据流时简直是救命稻草。
想象你要处理一个无限序列,比如斐波那契数列:
function* fibonacci() {
let [prev, curr] = [0, 1];
while (true) {
yield curr;
[prev, curr] = [curr, prev + curr];
}
}
const fib = fibonacci();
console.log('前 10 个斐波那契数:');
for (let i = 0; i < 10; i++) {
console.log(fib.next().value);
}
console.log('再取 5 个:');
for (let i = 0; i < 5; i++) {
console.log(fib.next().value);
}
这个无限序列用普通函数绝对实现不了,但 Generator 可以,因为它不会真的无限计算,只是准备好了"随时可以计算"的状态。
实际应用场景:处理大文件读取。比如你要读一个 10GB 的日志文件,不可能一次性读进内存:
function* readLines(fileContent) {
const lines = fileContent.split('\n');
for (const line of lines) {
yield line;
}
}
function* filterErrors(lineGenerator) {
let count = 0;
for (const line of lineGenerator) {
if (line.includes('ERROR')) {
yield line;
count++;
if (count >= 100) break;
}
}
}
const hugeLog = 'INFO: xxx\nERROR: 数据库连接失败\nINFO: yyy\nERROR: 请求超时\n...';
const errorLines = filterErrors(readLines(hugeLog));
for (const error of errorLines) {
console.log('发现错误:', 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';
}
;
:
result = { state, : [, ], : order };
(result. === ) {
state = ;
order. = ();
order;
}
;
:
:
:
{ state, : [], : order, : };
;
:
();
}
}
}
order = { : , : };
machine = (order);
step = machine.();
.(step.);
step = machine.({ : });
.(step..);
step = machine.({ : });
.(step..);
step = machine.({ : });
.(step..);
.(step.);
这种写法的好处是:
- 状态流转可视化:代码结构就是状态图
- 非法操作天然防呆:当前状态不支持的操作,你根本传不进去(因为 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';
}
}
}
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 });
}
};
}
浏览器兼容性翻车现场:IE?别提了,提就是泪
说了这么多好处,该泼冷水了。Generator 是 ES6 特性,IE 全系不支持,包括 IE11。如果你还要兼容 IE,要么用 Babel 转译(会生成很丑的 state machine 代码),要么就别用。
现代浏览器(Chrome、Firefox、Safari、Edge)都支持得很好,但老项目迁移要注意。而且 Generator 转译后的代码调试起来很痛苦,sourcemap 有时候对不上。
function* myGen() {
yield 1;
yield 2;
}
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() {
const data = fetch('/api/user');
console.log(data);
}
function* goodExample() {
const data = yield fetch('/api/user');
console.log(data);
}
坑 2:yield 了非 Promise
如果你用的执行器(比如 co)期望 yield 的都是 Promise,但你 yield 了个普通值,有些执行器会报错,有些会当成已 resolve 的 Promise 处理。行为不一致,容易出 bug。
坑 3:并行执行
Generator 是顺序执行的,如果你想并行发多个请求,得用特殊技巧:
function* parallelRequests() {
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) {
const delay = Math.pow(2, attempt - 1) * 1000;
console.log(`等待${delay}ms 后重试...`);
yield new Promise(resolve => setTimeout(resolve, delay));
}
}
}
();
}
() {
iterator = ();
() {
(result.) .(result.);
.(result.).(
(iterator.(res)),
(iterator.(err))
);
}
(iterator.());
}
failCount = ;
= () => ( {
failCount++;
(failCount <= ) {
( ());
} {
({ : , : .({ : }) });
}
});
(* () {
(, {}, );
})
.( .(, data))
.( .(, err));
这个实现的好处是:重试逻辑和业务逻辑完全分离,而且支持"取消"——如果你不想重试了,直接不调用 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 });
}, 300);
});
}
function* infiniteScrollLoader() {
let pageNum = 1;
const pageSize = 20;
let isLoading = false;
let hasMore = true;
while (hasMore) {
if (isLoading) {
yield { type: 'loading', message: '正在加载...' };
;
}
isLoading = ;
{
response = (pageNum, pageSize);
hasMore = response.;
pageNum++;
isLoading = ;
{ : , : response., hasMore, : pageNum - };
} (error) {
isLoading = ;
{ : , : error. };
{ : };
}
}
{ : , : };
}
() {
[articles, setArticles] = ([]);
[hasMore, setHasMore] = ();
[loading, setLoading] = ();
loaderRef = ();
( {
loaderRef. = ();
();
}, []);
= () => {
(!loaderRef. || loading) ;
{ value, done } = loaderRef..();
(done) ;
(value. === ) {
();
} (value. === ) {
( [...prev, ...value.]);
(value.);
();
} (value. === ) {
();
}
};
(
);
}
Generator 在这里的作用是封装分页状态。你不需要在组件里维护 pageNum、hasMore 这些变量,Generator 自己记得当前读到第几页。组件只负责触发和展示,逻辑清晰很多。
配合 Redux-Saga 管理副作用:虽然现在没人用了但面试还考
Redux-Saga 是 Generator 在 React 生态里的高光时刻。虽然现在 TanStack Query、SWR 这些更流行,但理解 Saga 对面试和读老代码很有帮助。
核心概念:Saga 是"长期存活的 Generator",它监听 action,然后执行副作用(异步操作)。
import { take, put, call, fork, cancel } from 'redux-saga/effects';
function* loginFlow() {
while (true) {
const { payload } = yield take('LOGIN_REQUEST');
const task = yield fork(authorize, payload.username, payload.password);
const action = yield take(['LOGOUT', 'LOGIN_ERROR']);
if (action.type === 'LOGOUT') {
yield cancel(task);
yield call(clearToken);
}
}
}
function* authorize(username, password) {
try {
const token = yield call(api.login, username, password);
yield put({ type: 'LOGIN_SUCCESS', token });
yield call(storeToken, token);
(userSaga);
} (error) {
({ : , : error. });
}
}
这里的 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 2;
}
const gen = yieldsUndefined();
console.log(gen.next());
console.log(gen.next());
console.log(gen.next());
console.log(gen.next());
问题 2:如何获取 return 的值
return 的值在最后一次 next() 里,通过 value 字段携带,done 标记为 true。
function* withReturnValue() {
yield 1;
return '我是返回值';
}
const gen = withReturnValue();
console.log(gen.next());
console.log(gen.next());
问题 3:for…of 如何知道何时停止
for…of 底层就是不断调用 next(),直到遇到 done: true 就停。这也是为什么它拿不到 return 值——它看到 done: true 就退出了,不 care value 是啥。
这个结构虽然看起来啰嗦,但它是整个迭代器生态的基础。理解它,你就理解了 JS 中所有遍历行为的底层逻辑。
yield 后面跟表达式结果不对?作用域和求值时机搞清楚没
有个细节很多人栽过跟头:yield 的优先级和求值时机。
function* precedenceTrap() {
const a = 1;
const b = 2;
const result = yield a + b;
console.log(result);
}
const gen = precedenceTrap();
console.log(gen.next());
console.log(gen.next(10));
答案是 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());
g.next(100);
对,第一次 yield 出去的是 3,第二次传入的 100 被赋值给 r。所以 yield 优先级确实比 + 低,整个表达式先求值再 yield。
但如果你想 yield 一个表达式结果,最好用括号明确:
yield (a + b);
yield a || 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 '错误已处理';
}
}
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 => {
try {
return step(iterator.throw(err));
} catch (genErr) {
.(genErr);
}
}
);
}
(iterator.());
}
(internalCatch)
.( .(, val))
.( .(, err.));
如果执行器正确地用 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());
try {
console.log(gen.next());
} catch (err) {
console.log('外部捕获:', err.message);
}
console.log(gen.next());
注意:Generator 内部 throw 错误后,这个 Generator 就废了,不能再恢复。这和 Promise 不一样,Promise reject 了还能链式调用 catch。
情况 3:外部通过 throw() 方法注入错误
function* externalError() {
try {
yield 1;
yield 2;
} catch (err) {
yield `捕获外部错误:${err.message}`;
}
yield 3;
}
const gen = externalError();
console.log(gen.next());
console.log(gen.throw(new Error('外部来的')));
console.log(gen.next());
这个机制让 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);
}
}
}
const tree = {
value: 1,
children: [
{ value: 2, children: [{ value: 4 }, { value: 5 }] },
{ value: 3, children: [{ value: 6 }] }
]
};
console.log([...traverseTree(tree)]);
function traverseTreeNormal(node) {
const result = [node.value];
if (node.children) {
for (const child of node.children) {
result.push(...traverseTreeNormal(child));
}
}
return result;
}
无限递归的安全版本:
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);
}
这种写法的好处是:你可以随时暂停遍历,去做别的事,然后再继续。普通递归函数一旦开始就必须走完。
把普通函数"升级"成 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());
console.log(gen.next());
console.log(gen.next());
function withLogging(fn) {
return function* (...args) {
console.log(`[${fn.name}] 开始执行,参数:`, args);
const start = Date.now();
try {
const result = fn(...args);
const duration = .() - start;
.();
{ : , duration, result };
result;
} (error) {
duration = .() - start;
.(, error.);
{ : , duration, error };
error;
}
};
}
loggedFetch = (fetch);
这种包装模式在测试和调试时很有用,你可以"拦截"函数的执行过程,插入观察点。
和 async 函数互转:Generator → Promise → async 的变形记
最后聊聊 Generator 和 async/await 的关系。其实 async/await 就是 Generator + Promise 的语法糖。理解这个转换,对你理解底层很有帮助。
Generator 转 async:
function* oldStyle() {
const user = yield fetchUser();
const orders = yield fetchOrders(user.id);
return { user, orders };
}
function newStyle() {
return _asyncToGenerator(function* () {
const user = yield fetchUser();
const orders = yield fetchOrders(user.id);
return { user, orders };
})();
}
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);
} catch (error) {
reject(error);
;
}
(info.) {
(info.);
} {
.(info.).(
(, value),
(, err)
);
}
}
();
});
};
}
看到没?async 函数本质上就是一个自动执行 Generator 并返回 Promise 的包装器。这也是为什么 async/await 能处理异步——底层还是 Generator 那一套。
async 转 Generator:
反过来,如果你想把 async 函数改成 Generator(比如为了更细粒度的控制),可以这样做:
async function fetchData() {
const res = await fetch('/api/data');
const json = await res.json();
return json;
}
function* fetchDataGen() {
const res = yield fetch('/api/data');
const json = yield res.json();
return json;
}
虽然看起来 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 了,但理解原理对调试很有帮助…'
然后看着他懵逼的表情,深藏功与名。
反正我当年要是有人这么给我讲,能少熬好几个通宵。现在这文章写给你,算是积德了。拿去用,不用谢,请叫我雷锋。