小白也能搞懂的JavaScript 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

这种写法的好处是:

  1. 状态流转可视化:代码结构就是状态图
  2. 非法操作天然防呆:当前状态不支持的操作,你根本传不进去(因为 state machine 只接受特定 action)
  3. 历史状态可追踪:因为是在 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,经常会遇到:

  1. Call Stack 看不懂:因为 Generator 是"暂停-恢复"机制,调用栈不是传统的函数嵌套,而是同一个函数在不同时间点执行。你看到的栈信息可能是乱的。

  2. 变量作用域诡异:yield 前后的变量,在 DevTools 的 Scope 面板里显示方式很奇怪,有时候你明明赋值了,却显示 undefined,因为执行上下文还没创建完。

  3. 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 在这里的作用是封装分页状态。你不需要在组件里维护 pageNumhasMore 这些变量,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 });
  }
}

这里的 takeputcallforkcancel 都是"Effect",它们被 yield 出去后,由 Redux-Saga 的中间件解释执行。Generator 本身只是描述"我要做什么",实际执行由外部控制。

这种模式的优势:

  1. 可测试性:你可以不启动 saga,直接检查 yield 了哪些 effect
  2. 可组合性:saga 可以互相调用,像函数一样
  3. 可取消性:通过 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 是多少?

答案是 resultNaN。因为 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 能让你:

  1. 看懂老代码:Redux-Saga、co、Koa 1.x 这些还在维护的项目
  2. 面试装X:“你知道 async/await 底层是怎么实现的吗?”
  3. 处理特殊场景:需要"可中断迭代"、“惰性求值”、"状态机"的时候,Generator 依然是最佳选择

下次同事吹牛说"我用过协程",你就微微一笑:“哦,你说 Generator 啊?那玩意儿我天天用,不就是 function* 加个 yield 嘛,还能 throw 和 return,配合 Promise 能实现异步流程控制,不过现在有 async/await 了,但理解原理对调试很有帮助…”

然后看着他懵逼的表情,深藏功与名。

反正我当年要是有人这么给我讲,能少熬好几个通宵。现在这文章写给你,算是积德了。拿去用,不用谢,请叫我雷锋。

在这里插入图片描述

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐