之前一直有一个概念, async/await是一个语法糖,它的出现让代码写起来更加流畅,使用的话只需要知道它是一个"让异步代码写起来像同步"的方案,返回的是一个promise就够了,但如果深入去探索,你会发现一些很有意思的事情,比如今天说的这个co函数。
这篇文章分两部分:第一部分 从最朴素的想法出发,一步步把 co 迭代到生产级;第二部分逐行剖析 tj/co 的真实源码,看看工业级实现比我们的手写版多考虑了什么。
读之前再来回顾生成器的两个基本事实:
- 生成器
yield X会暂停 ,并把X通过g.next()的返回值吐出来; - 下次
g.next(value)会恢复 执行,并把value塞回给上一个yield表达式------这就是 yield 的"双向传值"。
第一部分:手写一个co函数
第 0 步:明确co的目标
co 的目标:让你能用同步的写法写异步。本来要这样写:
js
fetchUser()
.then(user => fetchPosts(user))
.then(posts => console.log(posts));
我们希望能这样写(用生成器):
js
co(function* () {
const user = yield fetchUser();
const posts = yield fetchPosts(user);
console.log(posts);
});
但是,如果按照现在的模式,我们需要一次一次手动的去next(),想要自动化的去驱动,那就是co 要做的事情。具体来说:拿到 yield 出来的 Promise → 等它 resolve → 把结果用 next(结果) 塞回去 → 生成器继续 → 拿到下一个 Promise...... 循环往复,直到生成器结束。
第 1 步:写一个最原始的生成器
什么都不说,先看代码:
js
function* gen() {
const user = yield fetchUser();
const posts = yield fetchPosts(user);
console.log(posts);
}
const g = gen();
const r1 = g.next(); // 启动,跑到第一个 yield,r1.value 是 fetchUser() 的 Promise
r1.value.then(user => { // 等它 resolve
const r2 = g.next(user); // 把 user 塞回去,跑到第二个 yield,r2.value 是第二个 Promise
r2.value.then(posts => {
g.next(posts); // 把 posts 塞回去,生成器执行完,打印 posts
});
});
这是一个最基础的生成器,但问题也很明显:yield 有几个,就要手写几层嵌套 。这不就是回调函数的另外一层外衣嘛?更好的方式是把这个"等 → 塞回 → 继续"的重复动作变成一个自动循环。
第 2 步:简化版自动化
最简单的实现自动化调用的方式:递归。具体的代码如下:
js
function co(generator) {
const g = generator();
function step(value) {
const result = g.next(value); // 把上次结果塞回去,推进到下一个 yield
// result = { value: 吐出来的 Promise, done: 是否结束 }
if (result.done) return; // 生成器跑完了,收工
result.value.then(res => {
step(res); // 等 Promise 好了,把结果塞回去,进入下一轮
});
}
step(); // 启动:第一次 next 不需要传值
}
这就是 co 的雏形。它把第 1 步那些嵌套全压成了一个递归循环:
js
co(function* () {
const user = yield fetchUser();
const posts = yield fetchPosts(user);
console.log(posts);
});
第 3 步:异常处理
上一个步骤中,只考虑了成功分支,现在需要添加异常处理。
首先引入生成器的第二个驱动方法:g.throw(err) 。它的作用是在生成器当前暂停的那个 yield 处抛出一个错误 ;如果那行外面包着 try,错误就会落进 catch。这正是异步错误能被同步 try/catch 捕获的底层机制。具体请看代码:
js
function co(generator) {
const g = generator();
function step(nextFn) {
let result;
try {
result = nextFn(); // nextFn 要么是 () => g.next(v),要么是 () => g.throw(e)
} catch (e) {
return Promise.reject(e); // 生成器内部没 catch 住,整体失败
}
if (result.done) {
return Promise.resolve(result.value); // 生成器的 return 值,作为最终结果
}
return Promise.resolve(result.value).then(
res => step(() => g.next(res)), // 成功:下一轮用 next 塞结果
err => step(() => g.throw(err)) // 失败:下一轮用 throw 塞错误
);
}
return step(() => g.next()); // 启动
}
相比第 2 步,这一版多了三个关键点:
错误处理对称。 成功走 g.next(res)、失败走 g.throw(err),且两者都回到同一个 step 继续递归驱动,哪怕 catch 里又 yield 也能接着推进。外层的 try/catch 则兜住生成器内部未被捕获的错误。
co 返回一个 Promise。 return step(() => g.next()) 让整个 co 调用的结果是一个 Promise:生成器正常跑完就 resolve(值是 return 值),中途有未捕获错误就 reject。这正好呼应那个事实------async 函数永远返回一个 Promise。
兼容普通值。 用 Promise.resolve(result.value) 包一层,让 yield 出来的即使是普通值(非 Promise)也能正常处理。
js
const willFail = () => Promise.reject(new Error('请求挂了'));
co(function* () {
try {
yield willFail();
} catch (e) {
console.log('catch 抓到了:', e.message); // catch 抓到了: 请求挂了
}
});
第 4 步:最终版本
整理一下最终的版本:
js
function co(generator) {
const g = generator();
function step(nextFn) {
let result;
try {
result = nextFn();
} catch (e) {
return Promise.reject(e);
}
if (result.done) {
return Promise.resolve(result.value); // 生成器的 return 值,作为最终结果
}
return Promise.resolve(result.value).then(
res => step(() => g.next(res)),
err => step(() => g.throw(err))
);
}
return step(() => g.next());
}
// ------ 验证:成功链路 + 错误链路 + 返回值 ------
const delay = (v, ms) => new Promise(r => setTimeout(() => r(v), ms));
co(function* () {
const a = yield delay(1, 100);
const b = yield delay(2, 100);
try {
yield Promise.reject(new Error('boom'));
} catch (e) {
console.log('抓到错误:', e.message); // 抓到错误: boom
}
return a + b;
}).then(result => {
console.log('生成器返回:', result); // 生成器返回: 3
});
把这个最终版和 async/await 的用法对比一下,差别只剩"语法 + 谁来调 step":
js
// 手写 co + 生成器 // async/await(引擎内置了 co)
co(function* () { async function () {
const a = yield delay(1, 100); const a = await delay(1, 100);
const b = yield delay(2, 100); const b = await delay(2, 100);
return a + b; return a + b;
}).then(r => {}); }().then(r => {});
| 手写 co | async/await |
|---|---|
function* |
async function |
yield |
await |
g.next(res) 塞回成功值 |
引擎内置 |
g.throw(err) 塞回错误 |
引擎内置(让 try/catch 生效) |
| co 返回 Promise | async 函数自动返回 Promise |
到这里,我们手写的 co 已经具备了生产级的骨架:自动驱动、错误处理、返回 Promise。接下来看看真实的 tj/co 是如何实现工业化的。
第二部分:tj/co 源码逐段剖析
tj/co 的核心文件 index.js 总共两百多行,有两个主要差异:一是更细致的错误处理结构,二是支持了 Promise 之外的多种"可 yield 值"。
入口:co 函数本身
js
function co(gen) {
var ctx = this;
var args = slice.call(arguments, 1);
// 把所有逻辑包进一个 Promise,避免 promise chaining 导致的内存泄漏
return new Promise(function(resolve, reject) {
if (typeof gen === 'function') gen = gen.apply(ctx, args);
if (!gen || typeof gen.next !== 'function') return resolve(gen);
onFulfilled(); // 启动
// ... onFulfilled / onRejected / next 定义在这里
});
}
这里有几个值得注意的设计:
整体包在 new Promise 里。 之所以用一个大 Promise 包住、而不是层层 .then 链下去,是为了避免 promise chaining 引发的内存泄漏,这是工业级实现才会在意的细节。
入参更宽容。 if (typeof gen === 'function') gen = gen.apply(ctx, args)------co 既接受"生成器对象",也接受"生成器函数"(是函数就先调用它拿到生成器对象)。后面那行 if (!gen || typeof gen.next !== 'function') return resolve(gen) 是兜底:如果传进来的根本不是生成器(没有 next 方法),就直接把它当普通值 resolve 掉,不报错。
成功路径:onFulfilled
js
function onFulfilled(res) {
var ret;
try {
ret = gen.next(res); // 把上一步结果塞回去,推进生成器
} catch (e) {
return reject(e); // 生成器内部抛错且没 catch,整体 reject
}
next(ret); // 把 {value, done} 交给 next 处理
return null;
}
gen.next(res) 把上一次 Promise 的结果塞回生成器(yield 的双向传值);如果生成器内部抛了未捕获的错误,catch 住并 reject 整个 co。拿到 {value, done} 后交给 next 决定下一步。
失败路径:onRejected
js
function onRejected(err) {
var ret;
try {
ret = gen.throw(err); // 把错误抛进生成器暂停处
} catch (e) {
return reject(e); // 生成器没 catch 住这个错误,整体 reject
}
next(ret);
}
onRejected 用 gen.throw(err) 把错误送回生成器暂停的那一行 ------如果那行外面有 try/catch,错误就被业务代码自己捕获了(于是生成器能继续 next);如果没捕获,gen.throw 会把错误继续往外冒,被这里的 catch 兜住、reject 整个 co。
onFulfilled 和 onRejected 结构对称,正好对应我们第 3 步里"next 与 throw 走统一路径"的设计。
调度核心:next
js
function next(ret) {
if (ret.done) return resolve(ret.value); // ① 生成器结束,用 return 值 resolve
var value = toPromise.call(ctx, ret.value); // ② 把 yield 出来的值转成 Promise
if (value && isPromise(value)) // ③ 是 Promise,就挂上成功/失败回调
return value.then(onFulfilled, onRejected);
return onRejected(new TypeError( // ④ 不是可识别的 yieldable,报错
'You may only yield a function, promise, generator, array, or object, '
+ 'but the following object was passed: "' + String(ret.value) + '"'));
}
这个 next 是整个 co 的调度中枢,四行对应四种情况:
① ret.done 为真------生成器跑完了,用它的 return 值 resolve 整个 co。和我们手写版完全一致。
② toPromise 是 tj/co 比我们手写版强大的关键。 我们的手写版只用 Promise.resolve(result.value) 处理"Promise 或普通值"两种;而 tj/co 把 yield 出来的值先送进 toPromise,统一转成 Promise------它支持的"可 yield 值"多得多(下一节细说)。
③ 转成 Promise 后,挂上 onFulfilled(成功)和 onRejected(失败)两个回调------这一行就是整个递归循环的"咬合点"。
④ 如果 yield 出来的东西连 toPromise 都转不了(既不是 Promise、也不是函数/数组/对象),就抛 TypeError 明确报错。
加分项:toPromise 支持多种"可 yield 值"
js
function toPromise(obj) {
if (!obj) return obj;
if (isPromise(obj)) return obj; // 已经是 Promise
if (isGeneratorFunction(obj) || isGenerator(obj)) // 生成器:递归用 co 处理
return co.call(this, obj);
if ('function' == typeof obj) return thunkToPromise.call(this, obj); // thunk 函数
if (Array.isArray(obj)) return arrayToPromise.call(this, obj); // 数组:Promise.all 并发
if (isObject(obj)) return objectToPromise.call(this, obj); // 对象:值并发后组装
return obj; // 其它,原样返回
}
这是 tj/co 真正比手写版"工业化"的地方。
- Promise------直接返回。
- 生成器 / 生成器函数 ------递归调
co处理,所以 co 支持嵌套的生成器。 - thunk 函数 ------一种
fn(callback)形式的旧式异步约定,thunkToPromise把它转成 Promise(这是 co 早期版本的主要支持对象,现在更推荐 Promise)。 - 数组 ------
arrayToPromise内部用Promise.all,于是yield [p1, p2]能并发等待多个 Promise。 - 对象 ------
objectToPromise把对象各个值并发跑完再按 key 组装回来,于是yield { a: p1, b: p2 }能拿到{ a: 结果1, b: 结果2 }。
这些"可 yield 值"的扩展,是 co 作为一个真实库的便利性所在,但它们都建立在最核心的那条递归循环之上------本质没变。
顺带一提:co.wrap
js
co.wrap = function (fn) {
createPromise.__generatorFunction__ = fn;
return createPromise;
function createPromise() {
return co.call(this, fn.apply(this, arguments));
}
};
co() 是"立即执行一个生成器",而 co.wrap(fn*) 是把一个生成器函数转成一个普通函数,调用它返回 Promise。对比一下就明白它对应什么:
js
const fn = co.wrap(function* (id) {
return yield fetchUser(id);
});
fn(1).then(user => {});
// 几乎等价于现代写法:
async function fn(id) {
return await fetchUser(id);
}
fn(1).then(user => {});
co.wrap 产出的"接受参数、返回 Promise 的函数",正是 async function 的形态。可以说 co.wrap 就是 async 关键字的前身。