生成器函数:被低估的 JavaScript 控制流利器
你有没有遇到过这样的场景?
写异步代码时,明明逻辑很简单,却要被.then()套来套去搞得晕头转向;处理大量数据时,内存爆了才发现不该一次性加载全部内容;想实现一个无限序列(比如自增 ID),结果发现传统函数根本“停不下来”。
这些问题的背后,其实都指向同一个根源:JavaScript 缺乏对函数执行过程的精细控制能力。
直到 ES6 引入了function*和yield—— 也就是我们今天要聊的主角:生成器函数(Generator Function)。
它不像const、箭头函数那样直观易用,也不像async/await那样广为人知。但它却是现代前端框架底层机制的重要基石。Redux-Saga 的任务调度、Koa 的洋葱模型、甚至早期 async 函数的 polyfill,背后都有它的影子。
更重要的是,一旦你真正理解了生成器,你就掌握了“暂停函数”这种在其他语言中属于高级特性的能力。而这,正是通往更高级编程范式的大门。
什么是生成器?从一次“不会立即执行”的调用说起
我们先来看一段看似普通、实则诡异的代码:
function* hello() { console.log('Hello'); yield 'world'; console.log('Goodbye'); } const gen = hello();猜猜看,上面这段代码运行后会输出什么?
答案是:什么都不会输出。
这和普通函数完全不同。通常情况下,函数一调用就会开始执行。但生成器函数不一样——它返回的是一个生成器对象(Generator Object),而不是直接执行函数体。
这个对象是个“懒家伙”,只有当你主动叫它动一下,它才会往前走一步。
怎么动?靠.next()方法。
console.log(gen.next()); // 输出: Hello // { value: 'world', done: false } console.log(gen.next()); // 输出: Goodbye // { value: undefined, done: true }第一次调用.next(),函数才真正启动,执行到第一个yield暂停,并把'world'作为value返回。
第二次调用.next(),从上次暂停的地方继续,打印'Goodbye',走到函数末尾,此时done: true,表示任务完成。
这种“按需驱动”的行为,就是生成器最核心的能力:可中断执行。
它不只是能暂停:双向通信与状态保持
很多人以为yield只是用来“吐出值”的,其实它还能“接收值”。
看这个例子:
function* talker() { const name = yield '你的名字是?'; console.log(`你好,${name}!`); const age = yield '你多大了?'; console.log(`${age}岁啊,真年轻!`); }现在我们一步步推进:
const t = talker(); console.log(t.next()); // { value: '你的名字是?', done: false } console.log(t.next('张三')); // 你好,张三! // { value: '你多大了?', done: false } console.log(t.next(25)); // 25岁啊,真年轻! // { value: undefined, done: true }注意第二、第三次调用.next(value)时传入的参数,它们会“填进”对应的yield表达式中。也就是说:
const name = yield '你的名字是?';这行代码的意思其实是:“我先把问题抛出去,等外面给我答案了,再赋值给name”。
这就形成了函数内外的双向通信,有点像协程之间的协作对话。
而且在整个过程中,函数内部的状态(比如变量name、执行位置)都被完整保留着,不会因为暂停而丢失。这是闭包也难以做到的精准控制。
真正的价值:惰性求值与无限序列
生成器最大的优势之一,是它可以实现惰性计算(Lazy Evaluation)—— 只有你需要的时候才去算下一个值。
举个经典例子:斐波那契数列。
如果用数组预生成前 10000 项,不仅耗时还占内存。但如果用生成器呢?
function* fibonacci() { let [prev, curr] = [0, 1]; while (true) { yield curr; [prev, curr] = [curr, prev + curr]; } }就这么简单。你可以让它一直生成下去,但每一步都是按需触发。
const fib = fibonacci(); console.log(fib.next().value); // 1 console.log(fib.next().value); // 1 console.log(fib.next().value); // 2 console.log(fib.next().value); // 3 console.log(fib.next().value); // 5甚至可以用for...of遍历(记得加终止条件!):
let count = 0; for (const n of fibonacci()) { console.log(n); if (++count === 10) break; }类似地,你可以轻松写出一个永不枯竭的唯一 ID 生成器:
function* idMaker() { let id = 1; while (true) yield id++; }这些在传统函数中几乎无法优雅实现的功能,在生成器面前变得轻而易举。
曾经的异步救星:生成器 + Promise 的黄金组合
虽然现在大家习惯用async/await,但在 ES2017 之前,JavaScript 并没有原生的“同步写法异步逻辑”。那时候,开发者是怎么破局的?
答案就是:生成器 + Promise + 执行器(Runner)
设想这样一个需求:
- 获取用户信息;
- 根据用户 ID 获取订单;
- 根据订单 ID 获取商品详情。
用 Promise 写,层层嵌套或链式调用,容易混乱。而用生成器,可以写出看起来像同步的代码:
function fetch(url) { return new Promise(resolve => setTimeout(() => resolve(`Data from ${url}`), 1000) ); } function* main() { console.log('开始获取用户...'); const user = yield fetch('/user'); console.log(user); console.log('开始获取订单...'); const order = yield fetch(`/order?uid=${user}`); console.log(order); console.log('开始获取商品...'); const product = yield fetch(`/product?oid=${order}`); console.log(product); return '全部完成'; }这段代码看起来是不是很像async/await?唯一的区别是用了yield而不是await。
但它不能直接运行,需要一个“推动器”来自动调用.next()并处理 Promise:
function run(generatorFn) { const iterator = generatorFn(); function go(result) { if (result.done) return Promise.resolve(result.value); // 把 value 当作 Promise 处理 return Promise.resolve(result.value).then(data => { return go(iterator.next(data)); }); } return go(iterator.next()); }然后就可以这样运行:
run(main).then(console.log); // 依次输出... // 最终输出: 全部完成这套模式后来被封装成著名的库co,也成为async/await的设计原型。
至今,Redux-Saga依然基于这一思想工作:
function* fetchUserSaga(action) { try { const user = yield call(fetch, `/api/users/${action.payload.id}`); const data = yield call([user, user.json]); yield put({ type: 'USER_LOADED', payload: data }); } catch (err) { yield put({ type: 'LOAD_FAILED', error: err }); } }这里的call、put都是 Effect 创建函数,Saga 中间件负责解释并执行它们,本质上还是通过.next()推动流程前进。
实战技巧:别踩这几个坑
尽管强大,但生成器并不适合所有场景。使用时要注意以下几点:
✅ 什么时候该用?
- 复杂流程编排:多个异步操作之间有复杂依赖或分支逻辑。
- 监听多个事件源:如 Redux-Saga 中
takeEvery、takeLatest监听 action 流。 - 大数据流处理:逐条读取文件行、数据库记录,避免内存溢出。
- 状态机建模:游戏回合、UI 步骤引导等有限状态切换。
❌ 什么时候不该用?
- 简单异步请求:完全可以用
async/await替代,更简洁。 - 高频调用的小函数:生成器有一定性能开销,小题大做。
- IE 兼容需求:IE 全系列不支持,必须转译。
⚠️ 调试注意事项
生成器的调用栈和普通函数不同,断点可能会“跳来跳去”。建议:
- 使用
debugger语句辅助定位; - 在支持的环境中启用“Async Stack Traces”;
- 结合日志输出中间状态,尤其是
yield前后的上下文。
为什么今天还要学生成器?
你可能会问:既然有了async/await,为什么还要费劲学生成器?
因为:
async/await是语法糖,生成器是底层机制。
就像你想精通 React,就不能只懂 JSX 而不懂 Virtual DOM 一样。不了解生成器,你就看不懂 Redux-Saga 的源码,也无法真正理解“副作用是如何被管理的”。
此外,生成器的能力远不止异步控制:
- 它可以作为迭代器工厂,为自定义数据结构提供遍历支持;
- 可以结合 Symbol.iterator 实现类的可迭代化;
- 在算法题中用于回溯、生成组合排列等场景也非常高效。
更重要的是,它体现了一种编程哲学:将控制权交还给外部调度器,实现更灵活的任务管理。
这种“协作式多任务”(Cooperative Multitasking)的思想,在 Node.js、Deno、甚至 WASM 中都有广泛应用前景。
小结:掌握生成器,就掌握了 JS 的“暂停键”
生成器函数或许不再是日常开发中的首选工具,但它的重要性从未减弱。
它是 JavaScript 中首个提供函数级暂停与恢复能力的特性,开启了协程式编程的大门。
通过function*和yield,我们可以:
- 实现惰性求值,高效处理无限序列;
- 构建自定义迭代器,增强数据抽象能力;
- 编写线性风格的异步逻辑,提升代码可读性;
- 支撑复杂的状态管理架构,如 Redux-Saga。
即使你不亲自写生成器,了解其原理也能帮助你更好地使用基于它的库,理解现代 JS 异步演进的脉络。
所以,下次当你看到function*的时候,不要绕道走。停下来,试着读懂它背后的控制流设计——那可能是你迈向高级 JavaScript 开发的关键一步。
如果你正在尝试理解 Redux-Saga 或 Koa 的中间件机制,不妨动手实现一个简单的 generator runner,你会发现,原来“魔法”背后,不过是一次.next()的推动而已。