- 异步编程
【免费下载链接】q
A promise library for JavaScript
Q 是 JavaScript 早期的 Promise 库,其 CHANGES.md 以版本为索引,完整记录了从 0.0.1 到 1.5.1 的 API 演进、破坏性变更与底层实现调整。本文以该变更日志为骨架,结合 q.js 源码与 spec/q-spec.js 测试,梳理 Q 1.x 系列的核心能力:Q.any、Q.Promise、长堆栈追踪、未处理拒绝追踪、noConflict与tap等,并还原 0.9.x 时代的关键 API 清理与命名演变,帮助你理解"现代 Promise 之前"的异步编程生态。
版本总览与演进主线
CHANGES.md 覆盖了 Q 从 0.0.1(初始版本)到 1.5.1(当前仓库版本,见 package.json 中"version": "1.5.1")的全部历史。整体演进可以划分为三条主线:
- API 从实验走向稳定:0.2.x 的
post/invoke之争、0.8.x 的call/apply/bind体系、0.9.0 的大规模 API 清理,最终收敛为 1.0 之后的稳定形态。 - 能力不断向 ES6 Promise 靠拢:
Q.Promise构造器(1.0.1)、Q.any(1.2.0)、race、allSettled等组合方法逐步补齐。 - 错误处理与调试体验持续增强:长堆栈追踪(0.8.5 起)、未处理拒绝追踪(0.9.4/1.3.0)、
done/nodeify等终止链式调用并暴露错误的手段。
1.0.0 的发布说明很能说明项目定位:"这几乎就是 0.9 的重发布,0.9 已进入温和的维护模式,理应获得官方 1.0 的身份。野心勃勃的 2.0 即将到来,但 0.9/1.0 已被广泛分发,需要长期支持。"(见 CHANGES.md 1.0.0 小节)。因此 1.x 系列本质上是 0.9 的稳定延续,CHANGES.md 中 1.x 的条目也主要围绕新增 API 与缺陷修复。
Q 1.x 系列:稳定期的功能补全
1.5.x:Q.any错误信息与 finally 校验
1.5.1有两处改动:
Q.any现在会在错误信息中注明"Q.any 参与其中",并且只包含最后一个被拒绝的 promise 的错误信息(由 Ivan Etchart 贡献)。查看 q.js 中any的onRejected处理:当所有 promise 都被拒绝时,会构造rejection.message = "Q can't get fulfillment value from any promise, all promises were rejected. Last error message: ...",这正是 1.5.1 所描述的行为。- 在测试中避免使用
domain.dispose,为 Node.js 9 做准备(由 Anna Henningsen 贡献),对应测试代码在 spec/q-spec.js 中。
1.5.0包含三项能力:
Q.any的错误信息取自最后一个被拒绝的 promise(延续 1.5.1 的基础工作)。- 如果传给
finally(即fin)的回调不是函数,现在会抛出异常(由 @grahamrhay 贡献)。在源码中Promise.prototype.fin定义于 q.js 附近,该校验保证了fin/finally的调用安全性。 - 长堆栈追踪的重大改进:现在可以在跨
throw/rethrow 时构造长堆栈。此前长堆栈只能追踪单次异步跳转(0.9.5 才支持完整历史追踪),1.5.0 之后即使错误被重新抛出,Q 也能把完整的异步历史拼接到error.stack上。
1.4.x:浏览器<script>场景的补强
1.4.1修复了一个阻止 Q 作为 Firefox 附加组件<script>使用的问题。Q 现在可以在任何提供window或self全局变量的环境中运行,并且优先使用window——因为附加组件的self是不可变的,且与window不同。对应实现位于 q.js:模块封装器先判断 CommonJS/RequireJS/SES,再回退到window/self分支。
1.4.0增加了noConflict支持(由 @jahnjw 贡献),用于<script>场景下释放全局Q名称:
// 在 <script> 标签中使用时 // 保存之前的全局 Q,然后挂载新的 Q var previousQ = global.Q; global.Q = definition(); global.Q.noConflict = function () { global.Q = previousQ; return this; };这段逻辑在 q.js 中可以直接看到:noConflict会把全局Q恢复为加载前的值,并返回当前的 Q 对象(可通过return this得到引用继续使用)。需要注意的是,Q.noConflict只在 Q 作为全局变量使用时才有效,q.js 中定义了静态版本的兜底:在非全局环境下调用会抛出"Q.noConflict only works when Q is used as a global"错误。
1.3.0:Node.js 未处理/已处理拒绝追踪
1.3.0 在 Node.js 中加入了未处理(unhandled)与已处理(handled)拒绝的追踪(由 @benjamingr 贡献)。这其实是 0.9.4 引入的"未处理拒绝 API"的深化——现在 Q 会在拒绝被创建但从未被处理时,通过process.emit("unhandledRejection", reason, promise)通知 Node.js 运行时,并在拒绝稍后被处理时发出process.emit("rejectionHandled", ...)。
源码实现位于 q.js 的"UNHANDLED REJECTION TRACKING"区块:
trackRejection(promise, reason):在Q.reject构造拒绝 promise 时被调用(见 q.js),把 promise 与 reason.stack 记入内部数组。untrackRejection(promise):在拒绝被then的 rejection handler 消费时被调用(见 q.js)。- 配套的公开 API:
Q.getUnhandledReasons()(返回当前未处理拒绝的堆栈快照副本,见 q.js)、Q.resetUnhandledRejections()(清空追踪状态)、Q.stopUnhandledRejectionTracking()(关闭追踪)。
1.2.x:Q.any诞生与 npm 打包瘦身
1.2.0引入了Q.any(promisesArray)方法(由 @vergara 贡献):返回一个 promise,该 promise 以promisesArray中第一个被 fulfilled的 promise 的值作为 fulfillment 值;如果数组中所有 promise 都被拒绝,则返回被拒绝的 promise。这与all(全部 fulfilled 才成功、第一个拒绝即失败)和race(第一个 settle 者胜出)形成互补。
从 q.js 的实现可以看到细节:
- 空数组输入时直接
return Q.resolve()(fulfilled 的空值,不会拒绝); - 内部用
array_reduce遍历,每个输入都挂上onFulfilled/onRejected/onProgress; - 任何一个
onFulfilled命中就deferred.resolve(result)定局; - 只有
pendingCount归零(即全部被拒绝)才构造错误并deferred.reject; - 进度通知以
{index, value}形式透传,与Q.all的进度格式一致(见 q.js)。
1.1.2改为在 package.json 中使用"files"白名单(["LICENSE", "q.js", "queue.js"])替代.npmignore黑名单,从 npm 包里移除了多余文件(由 @anton-rudeshko 贡献)。这与 CHANGES.md 中 0.8.6 引入.npmignore的改动形成呼应:从"黑名单排除"到"白名单包含"的打包策略演进。
1.1.x:tap、Q_DEBUG与Q.nextTick可覆盖
1.1.0是一系列实用增强:
Q_DEBUG=1环境变量:在 Node.js 中设置Q_DEBUG=1即可全局开启长堆栈追踪。对应代码在 q.js:
if (typeof process === "object" && process && process.env && process.env.Q_DEBUG) { Q.longStackSupport = true; }tap方法:promise 上的tap(callback)会在值经过时"看一眼"而不改变它,适合插入日志、埋点等副作用操作。定义见 q.js(Q.tap与Promise.prototype.tap)。- 用
instanceof识别自有 promise:Q 现在通过instanceof Promise判断"自己的" promise 实例,而不是把所有 thenable 都当作 Q promise(见 q.js 中Q(value)的分支逻辑)。 - 超时错误带
code === "ETIMEDOUT":Promise.prototype.timeout构造的错误对象会设置error.code = "ETIMEDOUT"(见 q.js),方便程序化判断超时原因。 Q.nextTick可被用户覆盖:允许使用者替换 Q 内部的任务调度实现。- 同时移除了 Node.js 0.6/0.8 的持续集成,原因是 npm 的
^版本谓词运算符在传递依赖中无法在这些旧版本上使用。
1.1.1修复了启动(bootstrapping)过程中的两处回归:一处破坏了 WebWorker 支持,另一处完全破坏了<script>用法(issue #607)。这与模块加载封装(q.js)的多种环境检测逻辑直接相关。
1.0.x:Q.Promise与 CSP 兼容
1.0.1是 1.x 的重要里程碑:
Q.Promise构造器:实现 ES6Promise构造器的常用用法(new Q.Promise(function (resolve, reject, notify) {...}))。源码中Q.Promise = promise(q.js),其实现位于function promise(resolver)(q.js)。CHANGES.md 同时说明:"Promise并没有一个合法的 promise 构造器,真正的实现要等到 Q 2.0"——即在 1.x 中该构造器与 ES6 语义仍有差异,需注意使用前提。- 移除 promise inspector 的 console 兜底:该方案"已不再具备任何可靠性"。
- CSP(内容安全策略)兼容:修复了禁止
eval的环境。Q 改用StopIteration全局变量来区分 SpiderMonkey 生成器与 ES6 生成器(假设二者永不同时存在),相关分支逻辑可见 q.js 的async实现。
1.0.0本质是 0.9 的正式稳定化,仅有少量细节调整(例如浏览器中无论window.Touch是否定义都尝试输出调试信息;移除promise.valueOf的弃用警告,因为浏览器内部多种方式调用它,无法区分需要迁移的用法)。
0.9.x:API 清理与错误处理的分水岭
0.9.x 是 Q 历史上最重要的演进期,CHANGES.md 对这段历史的记录也最为详尽。
0.9.0:大规模 API 清理
0.9.0 移除了一层被弃用/未文档化的方法,使 Q 更贴近 Mark Miller 的 TC39 并发 strawman 提案。下表是 CHANGES.md 中的完整映射(原文表格,未做删减):
| 0.8.x 方法 | 0.9 替代 |
|---|---|
Q.ref | Q |
call、apply、bind(*) | fcall/invoke、fapply/post、fbind |
ncall、napply(*) | nfcall/ninvoke、nfapply/npost |
end | done |
put | set |
node | nbind |
nend | nodeify |
isResolved | isPending |
deferred.node | deferred.makeNodeResolver |
Method、sender | dispatcher |
send | dispatch |
view、viewInfo | (无) |
(*)不鼓励使用thisp;调用方法时请用post或invoke。
配套的 API 变更还包括:
Q(value)现在是一个可调用的导出函数,是resolve的别名(q.js 中Q.resolve = Q);invoke在所有形态下都是send的别名(q.js 可看到Q.send/Q.mcall/Q.invoke的并排定义);- 不带方法名的
post行为等同于fapply; delete与set(前身put)不再有 fulfillment 值;- Q promise 不再被冻结(因为冻结会损害 V8 性能);
- 新增
thenReject作为thenResolve的对偶方法。
错误处理方面:长堆栈可以通过Q.stackJumpLimit = 0关闭;在 Node.js 中,进程退出时若有未处理的拒绝,会输出到控制台。
内部协议(Internals and Advanced):内部 promise 接口改为dispatchPromise(resolve, op, operands),减少了参数切片;内部操作码 "put" 改为 "set"、"del" 改为 "delete";新增Q.fulfill,它与Q.resolve的区别在于不穿透 promise、也不强转其他系统的 promise——promise 本身就是 fulfillment 值,仅建议在"想用一个带有 then 函数但同时不是 promise 的对象去 fulfill 一个 promise"时使用。
0.9.5~0.9.7:状态检视与生成器支持
0.9.5引入了inspect——以{state: "fulfilled" | "rejected" | "pending", value | reason}形式获取 promise 状态(见 q.js 与 q.js 的 inspect 实现);allSettled则把输入 promises 全部 settle 后的状态数组化。CHANGES.md 特别澄清了术语:"settled"指 fulfilled 或 rejected;"resolved"指一个 deferred promise 被 resolve 到另一个 promise,即"把命运密封给后继 promise 的命运"。
同时:长堆栈默认关闭(需显式Q.longStackSupport = true);长堆栈可以追踪 promise 的完整异步历史而不仅是单次跳转;引入spawn(立即调用的异步生成器);增加实验性的mapply、mcall、nmapply、nmcall方法调用同义词。
0.9.6修复了"inspect 实现之前的旧 Q promise 与兼容 Q promise 的识别"问题,并通过两条独立途径修复了无限异步强转循环:所有makePromise返回的 promise 都实现inspect(默认报告 "unknown" 状态);then/when的实现迁移到then,使强转职责集中在when。nextTick也被重构为"无论请求多少个新 tick,都在 Q 内部使用展开的微任务"(issue #316,@rkatic)。
0.9.7宣布q.min.js不再入库(但仍由 Grunt 与 NPM 构建产出,见 Gruntfile.js);修复Q.async与 ES6 生成器的兼容问题;修复nextTick影响 Safari 6.0.5 首次加载(涉及 iframe)的问题;引入passByCopy、join、race;控制台改为直接显示堆栈或错误消息而非Error对象;消除包装方法以提升性能;Q.all现在按 ES6 迭代的风格透传进度通知{value, index}。
0.9.1~0.9.4:API 打磨与未处理拒绝追踪
- 0.9.1:AMD 检测兼容 RequireJS 优化器的
namespace选项(issue #225);修复valueOf的副作用——isFulfilled、isRejected、isPending不再意外触发外部代码(issue #226)。 - 0.9.2:
timeout与delay现在会透传进度通知(issues #229、#229 相关);修复nbind真正绑定thisArg(issue #232)。 - 0.9.3:
Q.timeout的错误支持自定义错误消息(issue #270);在 Node.js 0.10 中从process.nextTick切换到setImmediate,修复调用栈爆栈问题(issues #254、#259);修复与浏览器中 Mocha 测试运行器的兼容(Mocha 引入了没有nextTick的假process全局,issue #267);部分修复未处理拒绝检测的误报(issue #252);Q.promise在收到非函数参数时提前抛出异常。 - 0.9.4:
isPromise与isPromiseAlike始终返回布尔值(issue #284);async支持 ES6 生成器(issue #288);清除 dispatch 方法中的重复拒绝(issue #238);引入未处理拒绝 API(issue #296,@domenic):stopUnhandledRejectionTracking、getUnhandledReasons、resetUnhandledRejections——这正是 1.3.0 扩展的基础。
0.8.x:命名大调整与 Node 集成
0.8.x 是命名与 API 形态剧烈变动的时期,很多 0.9 时代的名字都源于此时。
- 0.8.0:移除
enqueue(用nextTick)、def(用master)、spy(用fin)、wait(用all(args).get(0))、join(用all(args).spread(callback));delay同时接受(value, timeout)与(timeout)两种签名(该双签名逻辑延续至今,见 q.js);新增spread;新增defer().node()与node/ncall(Node 风格回调适配)。 - 0.8.3:promise 原型新增
isFulfilled、isRejected、isResolved;新增allResolved(等待所有 promise 要么 fulfilled 要么 rejected,不传播错误,issue #53);node导出改名nbind;Method改名sender;浏览器控制台输出未处理错误的实时列表;支持msSetImmediate/setImmediate作为浏览器端nextTick;实验性别名finally(对应fin)、catch(对应fail)、try(对应call)、delete(对应del)。 - 0.8.4:
promise.timeout的拒绝值从字符串改为Error对象,消息中包含超时毫秒数(CHANGES.md 明确标注这是一处"未文档化、未指定公共行为"的变更,可能影响依赖字符串异常的用户);新增deferred.makeNodeResolver()取代晦涩的deferred.node();新增实验性Q.promise(maker(resolve, reject))(受 @gozala 的流构造器模式与 Windows Metro Promise 构造器接口启发,这就是Q.Promise的前身)与Q.begin()。 - 0.8.5:初步支持长堆栈(@domenic);新增
fapply/fcall/fbind无 thisp 版本;被拒绝的 promise 拥有exception属性;引入 Jasmine 规范(即本仓库的 spec/ 目录)。 - 0.8.6:修复
npost/ninvoke的thisp传递;修复多种非常规拒绝原因的处理;修复错误行为自定义 promise 的双重 resolve;加速Q.all(对已 resolve 的 promise 或标量值);堆栈过滤支持资源合并(issue #93);为弃用方法添加警告;添加.npmignore瘦身依赖包。 - 0.8.9:新增
nend;初步进度通知支持(then(onFulfilled, onRejected, onProgress)、progress(onProgress)、deferred.notify(...));put/del返回被操作对象以支持链式调用。 - 0.8.10:
done作为end的替代(等价于then(f, r, p).end());Q.onerror可设置的错误陷阱(用于获取未捕获错误的完整堆栈);thenResolve快捷方式(issue #108);进度通知的透传与转换改进;nend改名nodeify;deferred.resolve/deferred.reject不再(有时)返回deferred.promise;修复spread在被拒绝 promise 上不调用拒绝处理器的缺陷。 - 0.8.11:新增
nfcall/nfapply/nfbind(ncall/napply/nbind弃用);长堆栈不再导致链式 promise 时内存线性增长(issue #111);拒绝处理器中检查error.stack现在能得到长堆栈(issue #103);修复Q.timeout在 promise 被拒绝时未清理定时器导致事件循环存活的缺陷(issue #145);新增q/queue模块(无限 promise 队列构造器,见 queue.js)。 - 0.8.12:外部 promise 在
Q.isFulfilled中被视为未 resolve,使Q.all可以处理含外部 promise 的数组(issue #154);修复与 Promises/A+ 规范及测试套件的小型不一致(issues #157、#158,对应仓库中的 spec/aplus-adapter.js)。
0.7.x 及更早:语源与根基
更早的版本记录了许多至今仍在影响 Q 形态的决策:
- 0.7.0:移除
report与asap;fin的 callback 不再接收任何参数;enqueue改名nextTick;修复MessageChannel用于nextTick的 bug;shim 改为外部应用;实验性view/viewInfo。 - 0.6.0:
spy改名/别名fin;ref以Q构造器形式导出;新增async装饰器(用yield来"蹦床" promise,即生成器支持的开端);when、all变为可链式。 - 0.5.x:新增
all,join/wait重构为基于它,全部在最早拒绝处拒绝;fail、fin、spy加入 Q 与 promise 原型;def改名master;浏览器优先用MessageChannel做 next tick;end结束 promise 链(让传播的拒绝被抛出,从而触发 Node 的uncaughtException与浏览器的onerror)。 - 0.4.x:
end不再返回 promise;异常被消费后不再自动报告(必须显式通过.end()或.then(null, Q.error));支持 RequireJS;<script>场景创建Q全局变量。 - 0.3.0:
post恢复为 Tyler Closeref_sendAPI 的原始双参数签名(第二个参数通常是参数数组),新invoke提供可变参数调用;Promise构造器改名makePromise;术语细化:isResolved改名isFulfilled,新isResolved表示"非 promise 或已 settled"。 - 0.2.x:
post从(ref, name, args)改为可变参数(ref, name, ...args)(0.2.0 破坏性变更);thenable 采纳(0.2.6,使 Q 符合 Promises/A、B、D);promise 变为鸭子类型(promiseSend(op, resolved, ...)与valueOf,0.2.5);拒绝也鸭子类型化(promiseRejected === "true"且带reason);新增q/queue、q/util模块。 - 0.0.x~0.1.x:初始版本与
asap的引入/移除(0.1.0 因"坏了,大概坏在哲学上"而移除)。
这段历史揭示了 Q 的深层设计哲学:promise 不只是一个 API,而是一套可跨库交换的协议(鸭子类型、thenable 采纳、消息派发),这为后来 ES6 Promise 的标准化提供了实践基础。
用源码验证 1.x 核心 API 的当前形态
以下能力在 q.js 中均可找到对应实现,可作为阅读 CHANGES.md 时的对照索引:
| CHANGES.md 提及的能力 | 当前实现位置 | 关键行为 |
|---|---|---|
Q.any | q.js | 空数组直接 resolve;首个 fulfilled 胜出;全部拒绝才 reject,错误信息含 "Q can't get fulfillment value from any promise" |
Q.Promise | q.js | ES6 风格(resolve, reject, notify)构造器,异常自动转为拒绝 |
| 未处理拒绝追踪 | q.js | unhandledRejection/rejectionHandled事件 +getUnhandledReasons/resetUnhandledRejections/stopUnhandledRejectionTracking |
| 长堆栈支持 | q.js | Q.longStackSupport默认 false;Q_DEBUG=1环境变量自动开启;Q.stackJumpLimit = 0可关闭 |
tap | q.js | 观察值经过而不改变 |
timeout | q.js | 拒绝值带code: "ETIMEDOUT";拒绝/完成时清理定时器 |
delay | q.js | 双签名(value, ms)/(ms);拒绝立即透传 |
| Node 适配 | q.js | nfapply/nfcall/denodeify/nbind/ninvoke/npost/nodeify+deferred.makeNodeResolver |
| 生成器支持 | q.js | async(生成器 → promise 蹦床)与spawn(立即执行并done) |
| 环境加载 | q.js | CommonJS / RequireJS / SES /<script>(含noConflict)多环境封装 |
配套测试集中在 spec/q-spec.js(Jasmine 行为规范)与 spec/aplus-adapter.js(Promises/A+ 符合性适配器),运行方式见 package.json 的 scripts:npm test(jasmine-node + promises-aplus-tests + jshint)与npm run test-browser(opener spec/q-spec.html)。
小结:从 CHANGES.md 看 Q 的设计遗产
纵览 CHANGES.md,Q 的演进有四个反复出现的主题,至今仍具参考价值:
- 命名即文档:
resolve/fulfill/settle的术语区分(0.9.5)、isResolved→isFulfilled/isPending的细化(0.9.0、0.3.0),说明 API 命名需要精确承载语义。 - 错误必须可见:从
end/done终止链、Q.onerror,到未处理拒绝追踪(0.9.4 → 1.3.0)与长堆栈(0.8.5 → 1.5.0 跨 rethrow),Q 一直在解决"异步错误被静默吞掉"这一核心痛点。 - 稳定与实验并存:
Q.Promise、Q.any等能力先以实验形态出现,再逐步稳定;同时大量实验性 API(view、sender、nend)被及时清理。 - 协议优于实现:鸭子类型 promise、thenable 采纳、内部消息派发协议(0.2.x~0.9.x 一脉相承),让 Q 能与 jQuery、Dojo、When.js、WinJS 等互操作(README.md 的 Getting Started 一节有说明)。
对于现代开发者,CHANGES.md 的价值在于:它是理解"ES6 原生 Promise 之前"异步编程生态的第一手史料,也是阅读 q.js 源码时的路线图——每一个 1.x API 都能在这份变更日志里找到它的来龙去脉。
- 异步编程
【免费下载链接】q
A promise library for JavaScript
相关推荐
actix-web 版本演进全景解读:从 CHANGES.md 看 Actix Web 4.x 核心 API 变迁与升级指南
actix web 版本演进全景解读:从 CHANGES.md 看 Actix Web 4.x 核心 API 变迁与升级指南 导读 本文以 actix web
后端Web框架如何用ZMK开源键盘固件打造你的终极定制化机械键盘:从入门到精通完整指南
如何用ZMK开源键盘固件打造你的终极定制化机械键盘:从入门到精通完整指南 厌倦了千篇一律的键盘布局?想要一个真正懂你工作习惯的键盘吗?ZMK开源键盘固件就是你的
固件嵌入式智能硬件蓝牙GSYVideoPlayer 版本演进全解析:从 1.x 到 13.x 的核心能力变迁与 API 实践指南
GSYVideoPlayer 版本演进全解析:从 1.x 到 13.x 的核心能力变迁与 API 实践指南 导读 本文以仓库 doc/UPDATE_VERSIO
音视频移动开发
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考