从“暂停”开始理解JavaScript:Generator函数的实践与思考
你有没有想过,一个函数执行到一半能停下来,等你想让它继续的时候再接着运行?这听起来像是科幻电影里的桥断点续传,但在 JavaScript 中,这种能力真实存在——它就是Generator 函数。
在 ES6(ECMAScript 2015)发布之前,JavaScript 的函数一旦调用,就会从头跑到尾,中间无法暂停。回调嵌套层层叠加,“回调地狱”让代码变得难以维护。而 Generator 的出现,打破了这一限制。它不是简单地改进语法,而是为语言引入了一种全新的执行模型:协程(Coroutine)。
为什么需要“可暂停”的函数?
想象这样一个场景:你要依次加载用户信息、用户的订单列表和推荐商品数据。传统写法可能是这样的:
fetchUser((user) => { fetchOrders(user.id, (orders) => { fetchRecommendations(user.pref, (recs) => { renderDashboard(user, orders, recs); }); }); });缩进越来越深,逻辑分散,错误处理几乎无从下手。
后来 Promise 出现了,写法变成链式调用:
fetchUser() .then(user => Promise.all([user, fetchOrders(user.id), fetchRecommendations(user.pref)])) .then(([user, orders, recs]) => renderDashboard(user, orders, recs));虽然好了一些,但还是不够直观。
我们真正想要的是像写同步代码一样组织异步流程:
const user = yield fetchUser(); const orders = yield fetchOrders(user.id); const recs = yield fetchRecommendations(user.pref); renderDashboard(user, orders, recs);看起来是不是清晰多了?而这,正是 Generator + 执行器所能实现的效果。
Generator 是什么?用最简单的例子讲清楚
Generator 函数通过function*定义,调用后不会立即执行,而是返回一个遍历器对象(Iterator)。你可以手动控制它的每一步执行。
function* helloWorld() { yield 'Hello'; yield 'World'; return 'Ended'; }现在我们来“驱动”这个函数一步步运行:
const gen = helloWorld(); console.log(gen.next()); // { value: 'Hello', done: false } console.log(gen.next()); // { value: 'World', done: false } console.log(gen.next()); // { value: 'Ended', done: true }每次调用.next(),函数就向前走一步,直到遇到下一个yield或return。
-value表示当前产出的值;
-done表示是否已结束。
这就像你按下了播放键的录音机,每按一次快进,播放一句。
核心机制:不只是“暂停”,还能双向通信
普通函数只能单向输出结果(return),而 Generator 支持双向数据流动。
看这个例子:
function* echoMachine() { const a = yield 'Ready for input?'; console.log('Got:', a); const b = yield 'Another one?'; console.log('Got:', b); return 'Done.'; }启动并传入数据:
const it = echoMachine(); it.next(); // 启动,停在第一个 yield → { value: 'Ready...', done: false } it.next('Start!'); // 把 'Start!' 赋给 a → { value: 'Another...', done: false } it.next('Go ahead'); // 把 'Go ahead' 赋给 b → { value: 'Done.', done: true }注意关键点:.next(data)中的数据会作为上一个yield表达式的返回值。
这就意味着,外部可以影响内部逻辑流程。这种能力,在构建复杂状态流转或中间件系统时极为强大。
实战一:做一个无限计数器,却不卡死浏览器
利用惰性求值特性,我们可以轻松创建看似“无限”的序列,而不会阻塞主线程。
function* counter() { let n = 0; while (true) { yield ++n; } }使用时按需取值:
const c = counter(); console.log(c.next().value); // 1 console.log(c.next().value); // 2 console.log(c.next().value); // 3尽管是while(true),但由于每次只执行到yield就暂停,所以完全安全。类似思路可用于生成斐波那契数列、分页数据流、动画帧控制器等场景。
实战二:用 Generator 实现树结构中序遍历
对于非线性数据结构,比如二叉树,传统的递归遍历容易造成内存堆积。而 Generator 可以做到边访问边产出,节省资源。
class TreeNode { constructor(value, left = null, right = null) { this.value = value; this.left = left; this.right = right; } *inOrder() { if (this.left) yield* this.left.inOrder(); // 委托子Generator yield this.value; if (this.right) yield* this.right.inOrder(); } }使用方式和数组一样自然:
const root = new TreeNode(10, new TreeNode(5), new TreeNode(15) ); for (const val of root.inOrder()) { console.log(val); // 输出:5 → 10 → 15 }这里用了yield*,它可以将另一个可迭代对象的产出“转发”出来,非常适合递归结构。
实战三:用 Generator 组织异步流程,告别回调地狱
这是 Generator 最具革命性的应用场景之一。虽然现在有async/await,但理解它是如何基于 Generator 演进而来的,非常重要。
先定义一个模拟异步请求的函数:
function fetchData(url) { return new Promise(resolve => { setTimeout(() => resolve(`Data from ${url}`), 1000); }); }然后用 Generator 写出“同步风格”的异步逻辑:
function* asyncFlow() { console.log('Fetching user...'); const user = yield fetchData('/api/user'); console.log('Fetching posts...'); const posts = yield fetchData(`/api/posts?uid=${user.id}`); return { user, posts }; }但这段代码不会自动运行。我们需要一个“执行器”来驱动它:
function run(generatorFunc) { const iterator = generatorFunc(); function handle(result) { if (result.done) return Promise.resolve(result.value); return Promise.resolve(result.value).then(data => { return handle(iterator.next(data)); }); } return handle(iterator.next()); }最后启动:
run(asyncFlow).then(console.log);你会发现,整个过程像极了今天的async/await。事实上,co库就是这么干的,而redux-saga和早期Koa框架也都依赖这一模式。
Generator 在现代框架中的身影
别以为 Generator 已经过时了。恰恰相反,它在很多高级工具中扮演着底层角色。
Redux-Saga:用 Generator 管理副作用
function* loginSaga() { try { const credentials = yield take('LOGIN_REQUESTED'); const token = yield call(loginAPI, credentials); yield put({ type: 'SET_AUTH_TOKEN', token }); const profile = yield call(fetchProfile); yield put({ type: 'SET_PROFILE', profile }); yield call(saveToLocal, { token, profile }); } catch (err) { yield put({ type: 'LOGIN_ERROR', error: err.message }); } }这里的take,call,put都是 effect 创建函数,配合 middleware 解释执行。整个流程顺序清晰、易于测试、支持取消和竞态处理。
Koa.js:比 Express 更优雅的中间件模型
Koa 1.x 版本完全基于 Generator 实现中间件:
app.use(function *(next) { console.log('Before'); yield next; console.log('After'); });相比 Express 的next()回调模式,Koa 利用 Generator 实现了真正的“洋葱模型”,逻辑更直观。
使用建议与避坑指南
尽管功能强大,但使用 Generator 仍需谨慎:
✅ 推荐使用场景
- 构建自定义迭代器(如遍历图、DOM 树)
- 实现有限状态机(如表单验证、游戏 AI)
- 编排复杂的异步流程(尤其在 Redux-Saga 中)
- 生成大数据流或无限序列(惰性加载)
❌ 不推荐滥用的情况
- 简单的异步操作优先用
async/await - 避免在
.next()中进行大量同步计算,防止阻塞主线程 - IE 全系列不支持,必须通过 Babel 转译才能使用
- 调试体验较差,Chrome DevTools 对 Generator 的断点支持不如普通函数流畅
Generator 的意义:不止是语法糖
很多人说:“现在都用async/await了,还学 Generator 干嘛?”
其实不然。
Generator 是理解 JavaScript 异步演进的关键桥梁。没有它,就不会有co,不会有redux-saga,也不会催生出async/await的设计灵感。
更重要的是,它教会我们一种思维方式:把复杂流程拆解成可控的小步骤。无论是处理事件流、管理状态切换,还是实现懒加载算法,这种“分步推进”的思想都能带来更清晰的设计。
当你看到一段yield call(api)的代码时,你不只是在读一行语法,而是在观察一个暂停—恢复—传递数据的精密协程系统正在运行。
结语:通往协程世界的入口
Generator 函数或许不再是日常开发的首选,但它所承载的理念远未过时。它打开了 JavaScript 对协程和生成式编程的大门,让我们第一次真正拥有了对函数执行流的精细控制权。
如果你正在学习前端工程化、深入状态管理、或是研究响应式编程,那么理解 Generator,就是在打牢地基。
下一次当你写出async/await的时候,不妨想一想:背后那个曾被用来驱动异步流程的 Generator,是如何一步步引领我们走到今天的。
如果你在项目中用过
redux-saga或亲手写过 Generator 执行器,欢迎在评论区分享你的实战经验!