第一次看到 function* 这个写法的人,多半会卡在同一个地方,就是「函数怎么可能停在中间」。普通函数一旦调用就一路跑到 return,中途谁也拦不住。Generator 打破的正是这条规则,它把一个函数切成好几段,每段之间由外部决定什么时候继续。
理解了这件事,后面很多东西才能串起来。async/await 为什么被叫做语法糖,co 模块在做什么,Redux-Saga 那套 yield put() 的写法凭什么能测试,源头都在这里。这篇把 function*、yield、next 三个关键字的配合关系拆开讲,重点放在「谁在控制执行权」这条主线上,最后落到一个能自动跑异步流程的执行器上。
在本篇文章中,我们将从浅入深,和大家一起学习以下知识:
function*、yield、next三者到底各自负责什么- 一次
next()调用在引擎里走过的完整路径 yield表达式的返回值从哪里来,next(data)传参的意义yield*委托的真实语义,以及它等价于哪段for...of- 用 Generator 把异步写成同步样子的三类典型场景
- 手写一个
run自动执行器,理解async/await的原型 - 在任意对象上部署 Iterator 接口
# 一、什么是 Generator 函数
# 1.1 三个关键字
学习 Generator 语法,你需要先了解 function*、yield、next 三个基本概念。
function* 用来声明一个函数是生成器函数,它比普通的函数声明多了一个 *。* 的位置比较随意,可以挨着 function 关键字,也可以挨着函数名,两种写法引擎都认。团队里统一一种就行,我个人习惯 function* name() 这种,视觉上 * 跟 function 绑在一起,更像是在修饰「这是一种特殊的函数」。
yield 是产出的意思,这个关键字只能出现在生成器函数体内。生成器中也可以一个 yield 都没有,那样它就是一个调一次 next() 直接结束的生成器。函数遇到 yield 的时候会暂停,并把 yield 后面的表达式结果抛出去。
next 的作用是把代码的控制权交还给生成器函数。
这三个凑在一起,就构成了一套很朴素的协作关系:生成器函数负责标记「哪里可以停」,外部代码负责决定「什么时候继续」。
// 声明生成器函数
function* generator() {
// A
yield 'foo'
// B
}
// 获取生成器对象
let g = generator();
// 第一个 next(),首次启动生成器
g.next(); // {value: "foo", done: false}
// 唤醒被 yield 暂停的状态
g.next();
// {value: undefined, done: true}
这里有个细节很多人第一次会漏掉:generator() 这一行并不执行函数体,注释 A 处的代码此时一行都没跑。它只是返回了一个生成器对象。真正启动执行的是第一次 next()。
# 1.2 一次调用走了哪几步
拿一个更完整的例子来跟踪执行流程。
// 分析一个简单例子
function* helloGenerator() {
yield "hello";
yield "generator";
return;
}
var h = helloGenerator();
console.log(h.next()); // { value: 'hello', done: false }
console.log(h.next()); // { value: 'generator', done: false }
console.log(h.next()); // { value: undefined, done: true }
拆开看是这样的:
- 创建了
h对象,它是指向helloGenerator执行状态的一个句柄 - 第一次调用
next(),执行到yield "hello",暂缓执行,并返回了"hello" - 第二次调用
next(),从上次暂停的地方继续,执行到yield "generator",暂缓执行,并返回了"generator" - 第三次调用
next(),直接执行return,并返回done: true,表明结束
原文这里第三次的输出写成了 { value: 'undefined', done: true },带引号。实际返回的是原始值 undefined,不是字符串 "undefined",上面我已经改过来了。这种细节在面试手写输出结果的时候会被抓住。
经过上面的分析,yield 实际就是暂缓执行的标示,每执行一次 next(),相当于指针移动到下一个 yield 位置。

Generator 函数是 ES6 提供的一种异步编程解决方案。通过 yield 标识位和 next() 方法调用,实现函数的分段执行。
不过有一点要说清楚,Generator 本身跟异步没有任何关系。它只提供「暂停」和「恢复」这两个能力,是纯同步的语言特性。之所以能拿来处理异步,是因为有人在暂停的间隙里塞进了异步操作,等操作完成再调 next() 恢复。这个「有人」就是后面要讲的执行器。
# 1.3 yield 表达式与 yield*
yield 是 Generator 函数暂缓执行的标识,只能配合 Generator 函数使用,在普通的函数中使用会报错。
Generator 函数中还有一种 yield* 表达方式。
function* foo(){
yield "a";
yield "b";
}
function* gen(x, y){
yield 1;
yield 2;
yield* foo();
yield 3;
}
var g = gen();
console.log(g.next()); // {value: 1, done: false}
console.log(g.next()); // {value: 2, done: false}
console.log(g.next()); // {value: "a", done: false}
console.log(g.next()); // {value: "b", done: false}
console.log(g.next()); // {value: 3, done: false}
console.log(g.next()); // {value: undefined, done: true}
原文这段的输出注释写错了,把 yield* foo() 产出的三次都标成了 done: true。这是不对的。done 只有在生成器整个函数体执行完(走到 return 或者函数尾)之后才会变成 true。委托给 foo 的过程中外层 gen 显然还没结束,done 一直是 false。上面的输出我按实际行为重新标了一遍,最后补了一次拿到 {value: undefined, done: true} 的调用。
当执行 yield* 时,实际是遍历后面那个可迭代对象,等价于下面的写法:
function* foo(){
yield "a";
yield "b";
}
function* gen(x, y){
yield 1;
yield 2;
for (var value of foo()) {
yield value;
}
yield 3;
}
这里再纠正原文一句话。原文写的是「yield 后面只能适配 Generator 函数」,这个说法两处都不准确。
yield 后面可以跟任何表达式,数字、字符串、Promise、函数调用都行,没有任何限制。有限制的是 yield*,它后面要跟一个可迭代对象,也就是实现了 Symbol.iterator 的东西。Generator 对象当然是可迭代的,但数组、字符串、Map、Set 同样可以:
function* gen() {
yield* [1, 2]; // 依次产出 1、2
yield* 'ab'; // 依次产出 'a'、'b'
yield* new Set([9]); // 产出 9
}
console.log([...gen()]); // [1, 2, 'a', 'b', 9]
for...of 那个等价写法还有个不完全等价的地方,yield* 会把被委托生成器的 return 值作为整个 yield* 表达式的值返回,for...of 循环拿不到这个值。这个差别平时用不上,但在写嵌套流程的时候会咬人。
# 1.4 next 传参:真正的双向通信
前面讲的都是数据从生成器里往外流。反过来也可以,next(data) 传进去的参数,会成为上一个 yield 表达式的返回值。
function* echo() {
const a = yield 'first';
console.log('收到', a);
const b = yield 'second';
console.log('收到', b);
}
const it = echo();
it.next(); // {value: 'first', done: false},第一次 next 的参数会被丢掉
it.next('A'); // 打印「收到 A」,返回 {value: 'second', done: false}
it.next('B'); // 打印「收到 B」
第一次 next() 的参数是没有意义的,因为那时候还没有任何一个 yield 在等着接收值,函数体还没开始跑。我一开始也在这里绕过弯,写了个 it.next(初始值) 死活拿不到。
这个双向通道就是 Generator 能处理异步的关键。异步结果通过 next(result) 送回函数体内部,看起来就像是 yield 语句「返回」了异步的结果,同步写法的假象就是这么造出来的。
# 二、Generator 的应用场景
# 2.1 异步操作的同步化表达
Generator 函数暂停执行的效果,意味着可以把异步操作写在 yield 表达式里面,等到调用 next 方法时再往后执行。这实际上等同于不需要写回调函数了,因为异步操作的后续操作可以放在 yield 表达式下面,反正要等到调用 next 方法时再执行。所以 Generator 函数的一个重要实际意义就是用来处理异步操作,改写回调函数。
function* loadUI() {
showLoadingScreen();
yield loadUIDataAsynchronously();
hideLoadingScreen();
}
var loader = loadUI();
// 加载 UI
loader.next()
// 卸载 UI
loader.next()
上面代码中,第一次调用 loadUI 函数时,该函数不会执行,仅返回一个遍历器。下一次对该遍历器调用 next 方法,则会显示 Loading 界面,并且异步加载数据。等到数据加载完成,再一次使用 next 方法,则会隐藏 Loading 界面。
这种写法的好处是所有 Loading 界面的逻辑都被封装在一个函数里,按部就班非常清晰。以前写这类逻辑要在请求发起前 show、在成功回调里 hide、在失败回调里再 hide 一次,三处代码分散在不同的地方,漏一处就是 loading 转圈转到天荒地老。
通过 Generator 函数部署 Ajax 操作,也可以用同步的方式表达。