Generator?从 yield 卡壳,到终于搞懂协程那点事

刚开始接触生成器时,我对着function*和yield这两个奇怪的语法发呆了好久。尤其是看到 "生成器不是真正的函数" 这种说法,更是一头雾水。直到亲手写了几个例子,才慢慢摸透了它的脾气。

一、生成器(带星号的 "特殊函数")

先看一个最简单的生成器代码:

js 复制代码
function* generator() {
  console.log("enter");
  let a = yield 1;
  let b = yield (function () { return 2 })();
  return 3;
}
// 调用生成器函数,得到一个迭代器对象
var g = generator(); 
console.log(typeof g); // 输出object,不是function!

这第一行就颠覆了我的认知 ------ 调用生成器函数居然不执行函数体,还返回了个对象。后来才知道,生成器函数的作用不是直接执行代码,而是生成一个 "控制器"(迭代器),通过这个控制器来控制函数的执行节奏。

当我们调用g.next()时,神奇的事情发生了:

lua 复制代码
console.log(g.next());  
// 先输出"enter",再返回{ value: 1, done: false }
console.log(g.next());  
// 返回{ value: 2, done: false }
console.log(g.next());  
// 返回{ value: 3, done: true }
console.log(g.next());  
// 返回{ value: undefined, done: true }

这里的执行规律可以总结成三句话:

  1. 生成器函数被调用时,函数体不会执行,只会返回迭代器

  2. 每次调用迭代器的next(),函数体才会执行到下一个yield处暂停

  3. next()的返回值里,value是当前yield后面的结果,done表示是否执行完毕(遇到return后变成true)

最让我困惑的是let a = yield 1这句 ------ 第一次调用next()时,yield 1会返回 1,但a的值并没有被赋值。直到第二次调用next()时,传入的参数才会成为a的值。这个 "延迟赋值" 的特性,正是生成器能处理复杂逻辑的关键。

二、生成器的执行流程

为了理解生成器的执行流程,我画了个步骤图(纯手画的那种):

  1. 调用generator()生成迭代器g,函数体处于 "未执行" 状态

  2. 第一次g.next():函数开始执行,打印 "enter",遇到yield 1后暂停,返回{ value: 1, done: false }

  3. 第二次g.next():从暂停处继续,将next()的参数(这里没传,所以a是undefined)赋值给a,然后执行到yield 2处暂停,返回{ value: 2, done: false }

  4. 第三次g.next():继续执行,将参数赋值给b,直到遇到return 3,返回{ value: 3, done: true }

  5. 再调用next():函数已执行完毕,返回{ value: undefined, done: true }

如果给next()传参数,效果会更明显:

js 复制代码
const g = generator();
g.next(); // 到第一个yield暂停,如果传了参数会被忽略,因为没有上一个yield
g.next(10); // 把10赋值给a,然后执行到第二个yield
g.next(20); // 把20赋值给b,执行到return

这里的参数就像 "接力棒",每次调用next()时传给上一个yield的返回值。理解了这一点,才算真正入门生成器。

三、yield*

当一个生成器需要调用另一个生成器时,直接调用是没用的。比如我想让两个生成器按顺序执行:

js 复制代码
function* gen1() {
  yield 1;
  yield 4;
}
function* gen2() {
  yield 2;
  yield 3;
}

如果直接在gen1里写gen2(),调用时只会得到一个迭代器对象,不会执行gen2里的yield。这时候就需要yield*出场了:

js 复制代码
function* gen1() {
  yield 1;
  yield* gen2(); // 用yield*调用另一个生成器
  yield 4;
}
const g = gen1();
console.log(g.next().value); // 1
console.log(g.next().value); // 2(来自gen2)
console.log(g.next().value); // 3(来自gen2)
console.log(g.next().value); // 4

yield*的作用就像一根 "接力棒",把执行权交给被调用的生成器,直到它执行完毕,再把权交回来。这在处理复杂的迭代逻辑时特别有用,能让代码结构更清晰。

四、生成器的执行机制

为什么生成器能暂停和恢复执行?这就要说到 "协程" 这个概念了。

协程是比线程更轻量的存在,它运行在线程中,一个线程可以有多个协程,但同一时间只能有一个协程在工作。就像办公室里的打印机(线程),一次只能处理一个打印任务(协程),但任务可以暂停(比如中途换纸),然后继续执行。

生成器本质上就是协程的一种实现,它的执行过程可以用 "父子协程" 来理解:

js 复制代码
function* A() {
  console.log("我是A");
  yield B(); // A暂停,把执行权交给B
  console.log("结束了");
}
function B() {
  console.log("我是B");
  return 100; // B执行完毕,把执行权还给A
}
const gen = A();
gen.next(); // 输出"我是A" → 执行B → 输出"我是B"
gen.next(); // 输出"结束了"

这里的 A 是父协程,B 是子协程。当 A 执行到yield B()时,会暂停自己的执行,把线程的控制权交给 B。B 执行完毕后,又会把控制权还给 A,让 A 继续执行剩下的代码。

这种协作式的执行方式,没有线程切换的开销,效率非常高。这也是生成器能高效处理分步任务的原因。

五、让生成器的异步代码按顺序执行

生成器最实用的场景,就是处理异步操作。但它本身并不能直接处理异步,需要结合一定的技巧。

比如我们有两个异步读取文件的操作,第二个必须等第一个完成后才能执行。用回调函数会写成嵌套的样子,而用生成器可以写成 "同步" 的形式:

js 复制代码
// 模拟异步读取文件的函数
const readFile = (filename) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(`内容:${filename}`);
    }, 1000);
  });
};
// 生成器函数
function* asyncGen() {
  const data1 = yield readFile('file1.txt');
  console.log(data1);
  const data2 = yield readFile('file2.txt');
  console.log(data2);
}

但问题来了,生成器不会自动执行,每次都要调用next(),怎么让它一口气跑完呢?

1. 手动调用 next ()

最直接的方法是手动嵌套调用next():

js 复制代码
const gen = asyncGen();
gen.next().value.then(data1 => {
  gen.next(data1).value.then(data2 => {
    gen.next(data2);
  });
});

但这样写还是有嵌套,而且如果有多个异步操作,代码会很难看。

2. 封装自动执行函数

我们可以写一个工具函数,自动帮我们调用next():

js 复制代码
function run(gen) {
  const next = (data) => {
    let res = gen.next(data);
    if(res.done) return;
    // 处理普通值
    if(typeof res.value !== 'function') {
      console.log(res.value);  // 输出yield的值
      next();
    }
    // 处理函数(原逻辑)
    else {
      res.value(next);
    }
  }
  next();
}
// 调用
run(asyncGen()); 

这个函数用递归的方式,自动处理每个异步操作的结果,直到生成器执行完毕。

3. 使用 co 库

其实社区已经有成熟的工具了,比如 co 库,它的原理和我们上面写的run函数类似,但处理了更多边界情况:

js 复制代码
const co = require('co');
co(asyncGen).then(() => {
  console.log('所有异步操作完成');
});

一行代码就能让生成器自动执行完毕,是不是很方便?

六、总结一下

生成器虽然语法有点特别,但核心逻辑并不复杂:

  1. 生成器函数用function*声明,调用后返回迭代器

  2. 通过next()控制执行,yield负责暂停,return结束执行

  3. yield*用于调用另一个生成器,实现迭代接力

  4. 背后的协程机制让它能高效地进行分步执行

  5. 结合 Promise 和自动执行工具(如 co 库),能优雅地处理异步流程

另外记住:参数是上一个yield的返回值 这句话,就能慢慢搞懂Generator。

相关推荐
知了一笑4 分钟前
独立开发第二周:构建、执行、规划
java·前端·后端
UI前端开发工作室41 分钟前
数字孪生技术为UI前端提供新视角:产品性能的实时模拟与预测
大数据·前端
Sapphire~44 分钟前
重学前端004 --- html 表单
前端·html
Maybyy1 小时前
力扣242.有效的字母异位词
java·javascript·leetcode
遇到困难睡大觉哈哈1 小时前
CSS中的Element语法
前端·css
Real_man1 小时前
新物种与新法则:AI重塑开发与产品未来
前端·后端·面试
小彭努力中1 小时前
147.在 Vue3 中使用 OpenLayers 地图上 ECharts 模拟飞机循环飞行
前端·javascript·vue.js·ecmascript·echarts
老马聊技术1 小时前
日历插件-FullCalendar的详细使用
前端·javascript
zhu_zhu_xia1 小时前
cesium添加原生MVT矢量瓦片方案
javascript·arcgis·webgl·cesium
咔咔一顿操作1 小时前
Cesium实战:交互式多边形绘制与编辑功能完全指南(最终修复版)
前端·javascript·3d·vue