☰
co 4.x 完全指南:基于 Generator 与 Promise 的 Node.js 控制流库
2026/10/14 3:02:46 网站建设 项目流程
  • 后端
  • 开发工具

【免费下载链接】co

The ultimate generator based flow-control goodness for nodejs (supports thunks, promises, etc)

项目地址:https://gitcode.com/gh_mirrors/co/co
点击查看免费下载

导读

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)委托
Thunktypeof obj === 'function'thunkToPromise
数组Array.isArrayPromise.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的核心心智模型可以概括为三句话:

  1. 写同步风格:在function*里用yield表示"等待异步结果";
  2. 统一归一化:Promise、thunk、数组、对象、生成器六类对象经toPromise分派后,都以 Promise 形态被驱动循环消费;
  3. 错误双通道:异步错误经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)

项目地址:https://gitcode.com/gh_mirrors/co/co
点击查看免费下载
上一篇:Windows更新故障一键修复:Reset Windows Update Tool 保姆级使用教程
下一篇:像素世界"掉块"了?一次从慌乱到重生的Minecraft存档修复全记录

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询