
之前一直有一个概念async/await是一个语法糖它的出现让代码写起来更加流畅使用的话只需要知道它是一个“让异步代码写起来像同步”的方案返回的是一个promise就够了但如果深入去探索你会发现一些很有意思的事情比如今天说的这个co函数。这篇文章分两部分第一部分从最朴素的想法出发一步步把 co 迭代到生产级第二部分逐行剖析 tj/co 的真实源码看看工业级实现比我们的手写版多考虑了什么。读之前再来回顾生成器的两个基本事实生成器yield X会暂停并把X通过g.next()的返回值吐出来下次g.next(value)会恢复执行并把value塞回给上一个yield表达式——这就是 yield 的双向传值。第一部分手写一个co函数第 0 步明确co的目标co 的目标让你能用同步的写法写异步。本来要这样写fetchUser().then(userfetchPosts(user)).then(postsconsole.log(posts));我们希望能这样写用生成器co(function*(){constuseryieldfetchUser();constpostsyieldfetchPosts(user);console.log(posts);});但是如果按照现在的模式我们需要一次一次手动的去next()想要自动化的去驱动那就是co 要做的事情。具体来说拿到 yield 出来的 Promise → 等它 resolve → 把结果用next(结果)塞回去 → 生成器继续 → 拿到下一个 Promise…… 循环往复直到生成器结束。第 1 步写一个最原始的生成器什么都不说先看代码function*gen(){constuseryieldfetchUser();constpostsyieldfetchPosts(user);console.log(posts);}constggen();constr1g.next();// 启动跑到第一个 yieldr1.value 是 fetchUser() 的 Promiser1.value.then(user{// 等它 resolveconstr2g.next(user);// 把 user 塞回去跑到第二个 yieldr2.value 是第二个 Promiser2.value.then(posts{g.next(posts);// 把 posts 塞回去生成器执行完打印 posts});});这是一个最基础的生成器但问题也很明显yield 有几个就要手写几层嵌套。这不就是回调函数的另外一层外衣嘛更好的方式是把这个等 → 塞回 → 继续的重复动作变成一个自动循环。第 2 步简化版自动化最简单的实现自动化调用的方式递归。具体的代码如下functionco(generator){constggenerator();functionstep(value){constresultg.next(value);// 把上次结果塞回去推进到下一个 yield// result { value: 吐出来的 Promise, done: 是否结束 }if(result.done)return;// 生成器跑完了收工result.value.then(res{step(res);// 等 Promise 好了把结果塞回去进入下一轮});}step();// 启动第一次 next 不需要传值}这就是 co 的雏形。它把第 1 步那些嵌套全压成了一个递归循环co(function*(){constuseryieldfetchUser();constpostsyieldfetchPosts(user);console.log(posts);});第 3 步异常处理上一个步骤中只考虑了成功分支现在需要添加异常处理。首先引入生成器的第二个驱动方法g.throw(err)。它的作用是在生成器当前暂停的那个 yield 处抛出一个错误如果那行外面包着try错误就会落进catch。这正是异步错误能被同步try/catch捕获的底层机制。具体请看代码functionco(generator){constggenerator();functionstep(nextFn){letresult;try{resultnextFn();// nextFn 要么是 () g.next(v)要么是 () g.throw(e)}catch(e){returnPromise.reject(e);// 生成器内部没 catch 住整体失败}if(result.done){returnPromise.resolve(result.value);// 生成器的 return 值作为最终结果}returnPromise.resolve(result.value).then(resstep(()g.next(res)),// 成功下一轮用 next 塞结果errstep(()g.throw(err))// 失败下一轮用 throw 塞错误);}returnstep(()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也能正常处理。constwillFail()Promise.reject(newError(请求挂了));co(function*(){try{yieldwillFail();}catch(e){console.log(catch 抓到了:,e.message);// catch 抓到了: 请求挂了}});第 4 步最终版本整理一下最终的版本functionco(generator){constggenerator();functionstep(nextFn){letresult;try{resultnextFn();}catch(e){returnPromise.reject(e);}if(result.done){returnPromise.resolve(result.value);// 生成器的 return 值作为最终结果}returnPromise.resolve(result.value).then(resstep(()g.next(res)),errstep(()g.throw(err)));}returnstep(()g.next());}// —— 验证成功链路 错误链路 返回值 ——constdelay(v,ms)newPromise(rsetTimeout(()r(v),ms));co(function*(){constayielddelay(1,100);constbyielddelay(2,100);try{yieldPromise.reject(newError(boom));}catch(e){console.log(抓到错误:,e.message);// 抓到错误: boom}returnab;}).then(result{console.log(生成器返回:,result);// 生成器返回: 3});把这个最终版和 async/await 的用法对比一下差别只剩语法 谁来调 step// 手写 co 生成器 // async/await引擎内置了 coco(function*(){asyncfunction(){constayielddelay(1,100);constaawaitdelay(1,100);constbyielddelay(2,100);constbawaitdelay(2,100);returnab;returnab;}).then(r{});}().then(r{});手写 coasync/awaitfunction*async functionyieldawaitg.next(res)塞回成功值引擎内置g.throw(err)塞回错误引擎内置让 try/catch 生效co 返回 Promiseasync 函数自动返回 Promise到这里我们手写的 co 已经具备了生产级的骨架自动驱动、错误处理、返回 Promise。接下来看看真实的 tj/co 是如何实现工业化的。第二部分tj/co 源码逐段剖析tj/co 的核心文件index.js总共两百多行有两个主要差异一是更细致的错误处理结构二是支持了 Promise 之外的多种可 yield 值。入口co 函数本身functionco(gen){varctxthis;varargsslice.call(arguments,1);// 把所有逻辑包进一个 Promise避免 promise chaining 导致的内存泄漏returnnewPromise(function(resolve,reject){if(typeofgenfunction)gengen.apply(ctx,args);if(!gen||typeofgen.next!function)returnresolve(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 掉不报错。成功路径onFulfilledfunctiononFulfilled(res){varret;try{retgen.next(res);// 把上一步结果塞回去推进生成器}catch(e){returnreject(e);// 生成器内部抛错且没 catch整体 reject}next(ret);// 把 {value, done} 交给 next 处理returnnull;}gen.next(res)把上一次 Promise 的结果塞回生成器yield 的双向传值如果生成器内部抛了未捕获的错误catch住并reject整个 co。拿到{value, done}后交给next决定下一步。失败路径onRejectedfunctiononRejected(err){varret;try{retgen.throw(err);// 把错误抛进生成器暂停处}catch(e){returnreject(e);// 生成器没 catch 住这个错误整体 reject}next(ret);}onRejected用gen.throw(err)把错误送回生成器暂停的那一行——如果那行外面有try/catch错误就被业务代码自己捕获了于是生成器能继续next如果没捕获gen.throw会把错误继续往外冒被这里的catch兜住、reject整个 co。onFulfilled和onRejected结构对称正好对应我们第 3 步里next 与 throw 走统一路径的设计。调度核心nextfunctionnext(ret){if(ret.done)returnresolve(ret.value);// ① 生成器结束用 return 值 resolvevarvaluetoPromise.call(ctx,ret.value);// ② 把 yield 出来的值转成 Promiseif(valueisPromise(value))// ③ 是 Promise就挂上成功/失败回调returnvalue.then(onFulfilled,onRejected);returnonRejected(newTypeError(// ④ 不是可识别的 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 值functiontoPromise(obj){if(!obj)returnobj;if(isPromise(obj))returnobj;// 已经是 Promiseif(isGeneratorFunction(obj)||isGenerator(obj))// 生成器递归用 co 处理returnco.call(this,obj);if(functiontypeofobj)returnthunkToPromise.call(this,obj);// thunk 函数if(Array.isArray(obj))returnarrayToPromise.call(this,obj);// 数组Promise.all 并发if(isObject(obj))returnobjectToPromise.call(this,obj);// 对象值并发后组装returnobj;// 其它原样返回}这是 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.wrapco.wrapfunction(fn){createPromise.__generatorFunction__fn;returncreatePromise;functioncreatePromise(){returnco.call(this,fn.apply(this,arguments));}};co()是立即执行一个生成器而co.wrap(fn*)是把一个生成器函数转成一个普通函数调用它返回 Promise。对比一下就明白它对应什么constfnco.wrap(function*(id){returnyieldfetchUser(id);});fn(1).then(user{});// 几乎等价于现代写法asyncfunctionfn(id){returnawaitfetchUser(id);}fn(1).then(user{});co.wrap产出的接受参数、返回 Promise 的函数正是async function的形态。可以说co.wrap就是async关键字的前身。