- 后端
- 开发工具
【免费下载链接】co
The ultimate generator based flow-control goodness for nodejs (supports thunks, promises, etc)
导读
co是 Node.js 生态中极具代表性的控制流库:它让你以同步风格的yield编写异步代码,配合 Generator 函数自动完成"暂停—恢复"的调度。本文以仓库 Readme.md 为主线,结合 index.js 的完整实现与 test 目录下的测试用例,系统讲解co@4的 Promise 化改造、六类 yieldable 类型、co.wrap()包装 API、平台兼容性要求,以及背后的驱动引擎原理。读完本文,你将能独立使用co编写顺序、并行、嵌套的异步流程,并理解其与原生 async/await 的渊源。
Co v4:从 Thunk 到 Promise 的转折
co@4.0.0是该项目一个里程碑式的版本:底层实现全面转向 Promise,并被视为通往 ECMAScript async/await 提案的"踏脚石"。
核心 API 变化
v4 最关键的改动是co()的调用方式:
- v3 及以前:
co()返回一个 thunk,你需要再调用一次,传入回调函数和可选参数; - v4 起:
co()直接返回一个 Promise。
co(function* () { var result = yield Promise.resolve(true); return result; }).then(function (value) { console.log(value); }, function (err) { console.error(err.stack); });从 index.js 的实现可以看到,co()的签名是co(gen),但实际支持传入"Generator 函数、生成器,或任何返回生成器的函数":当typeof gen === 'function'时,会先以当前上下文和剩余参数调用它取得生成器对象;若得到的对象没有next方法(即不是生成器),则直接resolve(gen)。
生成器函数转普通函数:co.wrap
如果想保留"传入参数、返回 Promise"的常规函数形态,v4 引入了co.wrap(fn*):
var fn = co.wrap(function* (val) { return yield Promise.resolve(val); }); fn(true).then(function (val) { });co.wrap的实现(index.js)有一个值得注意的设计:它把被包装的 Generator 函数挂载到返回函数对象的__generatorFunction__属性上,从而避免每次调用co()都新建不必要的闭包:
co.wrap = function (fn) { createPromise.__generatorFunction__ = fn; return createPromise; function createPromise() { return co.call(this, fn.apply(this, arguments)); } };对应测试 test/wrap.js 验证了三件事:上下文能正确透传(co.wrap(fn).call(ctx)内部this等于ctx);参数能完整传递;__generatorFunction__属性确实指向原始function*函数。
平台兼容性:Promise 与 Generator 的前置条件
co@4+的运行依赖一个Promise实现,这是硬性前提。官方建议按环境区分处理:
| 运行环境 | 要求 |
|---|---|
node< 0.11及多数旧浏览器 | 必须自行引入Promisepolyfill |
| node 0.10.x 及更低版本、不支持 Generator 的浏览器 | 必须配合 gnode 和/或 regenerator 使用 |
| node 0.11.x | 必须加--harmony-generators或--harmony标志以启用 Generator |
| Node v4+ | 开箱即用,无需标志或 polyfill |
当前仓库 package.json 的engines字段同样印证了这一点:要求node >= 0.12.0且iojs >= 1.0.0。测试脚本也保留了历史痕迹,npm test运行的是mocha --harmony(package.json),这是 Generator 尚未默认开启时代的产物。
为什么 v4 必须依赖 Promise?
从 index.js 的注释可以看到一个重要的实现决策:co 把一切都包进一个 Promise 中,以避免 Promise 链式连接(chaining)导致的"内存泄漏"问题(对应 issue #180,见 History.md 4.1.0 条目)。整个驱动循环基于new Promise(function(resolve, reject){ ... })构建,因此没有原生 Promise 的环境无法运行。
安装与配套生态
$ npm install co安装后即可直接使用:
var co = require('co');任何返回 Promise 的库都可以和co良好配合。README 特别推荐了 mz、test/objects.js、test/recursion.js),例如:
var read = require('mz/fs').readFile; co(function *(){ var a = read('index.js', 'utf8'); var b = read('LICENSE', 'utf8'); var c = read('package.json', 'utf8'); var res = yield [a, b, c]; assert.equal(3, res.length); });核心用法示例
1. yield 任意 Promise
var co = require('co'); co(function *(){ // yield any promise var result = yield Promise.resolve(true); }).catch(onerror);2. 并行 resolve 多个 Promise(数组)
co(function *(){ var a = Promise.resolve(1); var b = Promise.resolve(2); var c = Promise.resolve(3); var res = yield [a, b, c]; console.log(res); // => [1, 2, 3] }).catch(onerror);3. 错误可以用 try/catch 捕获
co(function *(){ try { yield Promise.reject(new Error('boom')); } catch (err) { console.error(err.message); // "boom" } }).catch(onerror);其中onerror是一个兜底错误处理器,用于捕获所有未被 Generator 内部处理的异常:
function onerror(err) { // log any uncaught errors // co will not throw any errors you do not handle!!! // HANDLE ALL YOUR ERRORS!!! console.error(err.stack); }这是co一个极其重要的使用纪律:未处理的错误不会自动抛出,而是静默进入返回的 Promise;你必须用.catch(onerror)或 Generator 内的try/catch显式兜底,否则错误会被吞掉。测试 test/thunks.js 中的"should pass future errors to the callback""should pass immediate errors to the callback"等用例也验证了错误向.catch()传递的完整路径。
Yieldables:六类可 yield 对象全景
所谓 "yieldable",即你可以yield给co的合法对象类型。README 明确列出六类:
- promises(Promise 对象)
- thunks(单参数函数,即
function(done)形态) - array(数组,内部元素并行执行)
- objects(对象,属性值并行执行)
- generators(生成器对象,委托)
- generator functions(生成器函数,委托)
且嵌套 yieldable 完全支持——你可以把 Promise 嵌进对象、再嵌进数组,层层套叠。测试 test/recursion.js 验证了"数组套数组""对象套对象"的递归聚合能力:
var res = yield [a, [b, c]]; // res[1] 是 [b, c] 的结果数组 var res = yield { 0: a, 1: { 0: b, 1: c } };类型分派与转换原理
co之所以能统一处理这些类型,核心在于toPromise()分派函数(index.js):
function toPromise(obj) { if (!obj) return obj; if (isPromise(obj)) return obj; if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj); if ('function' == typeof obj) return thunkToPromise.call(this, obj); if (Array.isArray(obj)) return arrayToPromise.call(this, obj); if (isObject(obj)) return objectToPromise.call(this, obj); return obj; }分派优先级为:非空判断 → Promise → 生成器(委托给co递归执行)→ 函数(按 thunk 处理)→ 数组(并行)→ 纯对象(并行)→ 其他(原样返回,随后触发 TypeError)。
各类的判定与转换规则如下:
| 类型 | 判定方式(源码) | 转换实现 |
|---|---|---|
| Promise | 'function' == typeof obj.then(isPromise) | 直接返回 |
| 生成器 | 同时存在next与throw方法(isGenerator) | co.call(this, obj)委托 |
| 生成器函数 | 构造器名为GeneratorFunction,或原型具备生成器特征(isGeneratorFunction) | co.call(this, obj)委托 |
| Thunk | typeof obj === 'function' | thunkToPromise |
| 数组 | Array.isArray | Promise.all(obj.map(toPromise, this)) |
| 对象 | Object == val.constructor(纯对象,isObject) | 逐属性转换后Promise.all |
Promises
yield一个 Promise 是最基础、也是最推荐的用法。co 的驱动循环会等待 Promise 落定,成功则把结果作为下一次gen.next(res)的返回值,失败则调用gen.throw(err)注入到 Generator 内部(index.js)。test/promises.js 验证了:单 Promise、连续多个 Promise、Promise 拒绝后被try/catch捕获并继续执行等场景。
Thunks
Thunk 是只有一个参数(回调函数)的函数,其历史意义重大:它是co从 v1 到 v3 时代的核心抽象。在 v4 中,thunk 支持仅出于向后兼容保留,未来版本可能移除。
其转换逻辑(thunkToPromise)把fn(err, res)形态的回调转换为 Promise:
function thunkToPromise(fn) { var ctx = this; return new Promise(function (resolve, reject) { fn.call(ctx, function (err, res) { if (err) return reject(err); if (arguments.length > 2) res = slice.call(arguments, 1); resolve(res); }); }); }两个细节值得注意:回调第一个参数非空即拒绝;如果回调被传入多于两个参数(例如done(null, 'stdout', 'stderr')),则结果会被收集为数组——test/thunks.js 的 "with many arguments" 用例正是断言yield后得到['stdout', 'stderr']。
Arrays:并行执行
yield一个数组会并行resolve 其中所有 yieldable,结果保持原顺序返回:
co(function* () { var res = yield [ Promise.resolve(1), Promise.resolve(2), Promise.resolve(3), ]; console.log(res); // => [1, 2, 3] }).catch(onerror);实现上直接复用Promise.all(arrayToPromise)。test/arrays.js 还验证了空数组yield []得到空结果、数组内可以是生成器等场景。
Objects:属性并行执行
对象与数组同理,所有属性值的 yieldable 并行 resolve,结果保持键名:
co(function* () { var res = yield { 1: Promise.resolve(1), 2: Promise.resolve(2), }; console.log(res); // => { 1: 1, 2: 2 } }).catch(onerror);objectToPromise的实现(index.js)比数组稍复杂:它预先用new obj.constructor()构造同类型的结果容器,先定义好所有键(值暂为undefined),把可转换的属性转为 Promise 并入Promise.all,不可转换的普通值(字符串、数字、Date 实例等)直接原样复制。对应测试 test/objects.js 验证了三点:
- 混合普通值与 Promise 时,普通值原样保留("should ignore non-thunkable properties");
- 键顺序保持不变("should preserve key order"):即使不同属性的 thunk 延迟时间不同(30ms/20ms/10ms),
Object.keys结果顺序与原始对象一致; - 空对象
yield {}得到空对象。
Generators 与 Generator Functions:委托
任何能传给co的生成器或生成器函数,同样可以yield(委托给新的co执行):
function *work() { yield sleep(50); return 'yay'; } co(function *(){ var a = yield work; // 委托生成器函数 var b = yield work(); var res = yield [work, work, work]; // 并行委托 assert.deepEqual(['yay', 'yay', 'yay'], res); });README 特别提醒:这种用法应当尽量避免,因为社区正朝着符合规范的 Promise 方向演进(即 async/await)。test/generators.js 与 test/generator-functions.js 内容完全一致,分别从生成器对象与生成器函数两个入口验证委托与错误传播(生成器内throw new Error('boom')会被外层捕获)。
API 详解
co(fn*).then( val => )
co(fn*)返回一个 Promise,它会 resolve 一个生成器、生成器函数,或任何返回生成器的函数:
co(function* () { return yield Promise.resolve(true); }).then(function (val) { console.log(val); }, function (err) { console.error(err.stack); });关于入参,还有一个 v4.6.0 起新增的能力(见 History.md 4.6.0 条目):可以把剩余参数传入co并注入生成器:
function *gen(...args) { } co(gen, ...args);其实现就在co()的开头(index.js):var args = slice.call(arguments, 1);取出第一个参数之后的所有实参,随后gen.apply(ctx, args)将它们传给生成器函数。test/arguments.js 验证了数字、字符串、数组、对象、函数五类实参都能正确注入。
var fn = co.wrap(fn*)
co.wrap把生成器转换成返回 Promise 的普通函数:
var fn = co.wrap(function* (val) { return yield Promise.resolve(val); }); fn(true).then(function (val) { });其行为要点(index.js + test/wrap.js):
- 上下文透传:
wrapped.call(ctx, ...)后,生成器内this === ctx; - 参数透传:
wrapped(1, 2, 3)时生成器参数按原顺序接收; - 原函数暴露:可通过
wrapped.__generatorFunction__拿到原始生成器函数。
同时co.wrap也解决了 README 开头提到的需求:想用 thunk 风格调用、又想要 Promise 返回值时,用它把生成器函数"固化"为普通函数,避免每次调用都要包一层co(...)。
this 上下文:co.call(this, ...)
co会保留调用方的this上下文,并把它传给生成器执行(var ctx = this;,见 index.js)。测试 test/context.js 演示了标准用法:
var ctx = { some: 'thing' }; co.call(ctx, function *(){ assert(ctx == this); });这一特性对把co嵌入对象方法、需要维持接收者的场景非常有用。
驱动引擎:一个 50 行的手写 async/await
最后来看co最精彩的部分——index.js 中那个紧凑的驱动循环。整个执行机制浓缩在三个内部函数中:
- onFulfilled(res):调用
gen.next(res)把上一次 Promise 的结果"塞回"生成器,取得{ value, done },然后交给next(); - onRejected(err):调用
gen.throw(err)把错误"掷入"生成器内部(这正是yield处能用try/catch捕获异步错误的原理),再交给next(); - next(ret):若
ret.done为真,resolve(ret.value)收尾;否则把yield出的值经toPromise归一化为 Promise,挂上then(onFulfilled, onRejected)继续循环。
function next(ret) { if (ret.done) return resolve(ret.value); var value = toPromise.call(ctx, ret.value); if (value && isPromise(value)) return value.then(onFulfilled, onRejected); return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, ' + 'but the following object was passed: "' + String(ret.value) + '"')); }当yield了不受支持的值(如null、字符串、普通数字)时,co 会抛出上述TypeError。测试 test/invalid.js 用yield null验证了错误信息包含 "You may only yield";test/thunks.js 中 "when yielding neither a function nor a promise" 用例则验证了 yield 字符串会在 Generator 内被try/catch捕获。
这套机制本质上就是async/await 的手写原型:async function之于await,恰如 Generator +co之于yield。而 co 用 50 余行代码完成了 Promise 归一化、类型分派、并行聚合、递归嵌套与双向错误注入,这正是它作为"通往 async/await 的踏脚石"(README 原话)的底气所在。
小结
co的核心心智模型可以概括为三句话:
- 写同步风格:在
function*里用yield表示"等待异步结果"; - 统一归一化:Promise、thunk、数组、对象、生成器六类对象经
toPromise分派后,都以 Promise 形态被驱动循环消费; - 错误双通道:异步错误经
gen.throw注入可在try/catch中捕获,未被捕获的错误则汇入返回的 Promise,必须用.catch(onerror)兜底。
无论你是维护历史 Node.js 项目,还是想理解 Generator 协程与 async/await 之间的演进脉络,阅读本仓库的 index.js 与 test 目录都是极佳的学习路径——它把异步流程控制的本质浓缩在了一百多行代码里。项目以 MIT 协议开源(LICENSE),可放心阅读、安装与参考。
- 后端
- 开发工具
【免费下载链接】co
The ultimate generator based flow-control goodness for nodejs (supports thunks, promises, etc)
相关推荐
终极指南:深入理解co的Generator与Promise协同机制 🚀
终极指南:深入理解co的Generator与Promise协同机制 🚀 co 是Node.js中一个革命性的异步流程控制库,它巧妙地将Generator函数与
后端开发工具web3.js 4.x IPC Provider 完全指南:基于 Node.js 本地节点的安全 JSON-RPC 连接
web3.js 4.x IPC Provider 完全指南:基于 Node.js 本地节点的安全 JSON RPC 连接 web3 providers ipc
区块链Web3如何用co实现终极异步流程控制:Generator与Promise协同的完整指南
如何用co实现终极异步流程控制:Generator与Promise协同的完整指南 co是一个基于Generator的Node.js流程控制工具,通过Promis
后端开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考