- 后端
【免费下载链接】bluebird
:bird: :zap: Bluebird is a full featured promise library with unmatched performance.
导读
本文基于 Bluebird 官方文档 docs/docs/anti-patterns.md,系统剖析 Promise 编程中最常见的两大反模式——显式构造反模式(Explicit Construction / Deferred Anti-pattern)与.then(success, fail)反模式。文章不仅完整还原官方文档的示例代码与修正方案,还结合本仓库src/下的真实源码(如 src/promisify.js、src/catch_filter.js、src/promise.js)与测试用例,深入解释这些反模式为何危险、底层实现如何规避它们。读完你将掌握:何时必须用new Promise构造器、何时应该直接返回已有 Promise、Promise.promisify与Promise.fromCallback的正确打开方式,以及为什么.catch比.then(success, fail)更符合 Promise 的设计哲学。
一、理解 Promise 的核心价值:恢复同步代码的优良性质
在进入反模式之前,先明确 Promise 存在的意义。官方文档强调:Promise 的使命是让异步代码保留同步代码的大部分优良性质——扁平的缩进(flat indentation)和单一异常通道(one exception channel)。
当你把 Promise 当作"升级版事件发射器"或"回调工具"来使用时,就很容易落入反模式。这两个反模式本质上是同一类错误:没有用 Promise 的方式思考异步流程,而是把旧的回调心智模型硬套到 Promise 上。
二、反模式一:显式构造(Explicit Construction Anti-pattern)
这是最常见的 Promise 反模式,也被称为Deferred Anti-pattern(延迟对象反模式)。它的特征是在完全没有必要的情况下创建 Promise 对象,从而把代码复杂化。
2.1 典型错误 1:手里已有 Promise 却非要包一层 Deferred
下面是一个典型的 AngularJS + Restangular 场景(示例版权归 Twisternha 所有),函数内部明明已经通过getList()拿到一个 Promise,却仍然手动创建一个$q.defer()去包裹它:
myApp.factory('Configurations', function (Restangular, MotorRestangular, $q) { var getConfigurations = function () { var deferred = $q.defer(); MotorRestangular.all('Motors').getList().then(function (Motors) { //Group by Config var g = _.groupBy(Motors, 'configuration'); //Map values var mapped = _.map(g, function (m) { return { id: m[0].configuration, configuration: m[0].configuration, sizes: _.map(m, function (a) { return a.sizeMm }) } }); deferred.resolve(mapped); }); return deferred.promise; }; return { config: getConfigurations() } });这段代码的问题不仅仅是啰嗦。这种多余的包裹是危险的:任何在.then处理函数中抛出的错误、以及getList()本身的 rejection,都会被"吞掉",无法传播给这个函数的调用方。也就是说,调用方拿到的deferred.promise永远处于 pending 或错误状态,错误信息悄然丢失,排查问题会非常痛苦。
2.2 正确写法:直接返回已有的 Promise
修正方式非常简单——把已有的 Promise 直接返回,并用return来传递值:
myApp.factory('Configurations', function (Restangular, MotorRestangular, $q) { var getConfigurations = function () { //Just return the promise we already have! return MotorRestangular.all('Motors').getList().then(function (Motors) { //Group by Cofig var g = _.groupBy(Motors, 'configuration'); //Return the mapped array as the value of this promise return _.map(g, function (m) { return { id: m[0].configuration, configuration: m[0].configuration, sizes: _.map(m, function (a) { return a.sizeMm }) } }); }); }; return { config: getConfigurations() } });代码不仅更短,更重要的是:任何错误都会正确传播到最终的消费者。这正是"单一异常通道"性质的体现——在同步代码里你绝不会写var x = try { f() } catch ...再把结果包一层,异步代码同样不该这么做。
Bluebird 底层对 thenable(具有.then方法的对象)的转换也印证了这一点:在 src/thenables.js 的tryConvertToPromise中,当传入对象本身就是 Bluebird Promise 时会直接复用(if (obj instanceof Promise) return obj;),其他库的 Promise 也会通过_then快速接入,而不是包一层 deferred。
2.3 典型错误 2:手动包裹回调 API,还包得很糟糕
第二个典型错误是写一个函数,其唯一作用就是手动把一个回调风格的 API 包成 Promise,而且包得很不专业:
function applicationFunction(arg1) { return new Promise(function(resolve, reject){ //Or Q.defer() in Q libraryFunction(arg1, function (err, value) { if (err) { reject(err); } else { resolve(value); } }); }这被称为"重新发明方轮子"(reinventing the square wheel)。任何回调 API 的 promisification 都应该直接交给 Promise 库的泛化 promisification 方法:
var applicationFunction = Promise.promisify(libraryFunction);2.4 源码视角:为什么Promise.promisify更优
从源码看,Promise.promisify之所以推荐,是因为它远不止"帮你少写几行 if/else"。在 src/promisify.js 中:
- 性能优化:在非浏览器环境(
!__BROWSER__)下,Bluebird 通过makeNodePromisifiedEval动态生成针对参数个数优化的 switch-case 调用代码(见switchCaseArgumentOrder与generateArgumentSwitchCase),按函数fn.length推断最可能的参数个数并优先尝试,避免走昂贵的arguments收集路径。文档中"泛化 promisification 更快,因为它可以直接使用内部机制"的说法正源于此。 - 处理同步抛异常:源码中包裹后的函数用
util.tryCatch调用原始函数,若同步抛出(ret === errorObj),立即通过promise._rejectCallback(maybeWrapAsError(ret.e), true, true)转为 rejection。手写的包装常常忽略这一点。 - 处理多成功值:
Promise.promisify支持{multiArgs: true}选项。因为 Promise 只支持单个成功值,而某些回调 API 会以多个参数回调成功结果,开启multiArgs后 promise 会以成功值数组 fulfill。
Promise.promisify的完整签名(详见 docs/docs/api/promise.promisify.md):
Promise.promisify( function(any arguments..., function callback) nodeFunction, [Object { multiArgs: boolean=false, context: any=this } options] ) -> functionnodeFunction需符合 Node.js 约定:回调作为最后一个参数,且以 error-first 方式调用;multiArgs: true时 promise 始终以回调成功值数组 fulfill;- 传入
context时,nodeFunction会以该对象为this调用;也可通过promisified.call(obj, ...)动态指定。
测试用例 test/mocha/promisify.js 中可以看到这些选项的组合验证,例如Promise.promisify(successNodeMultipleValues, {multiArgs: true})、{multiArgs: true, context: THIS}等。
对于一整个库,Promise.promisifyAll会在每个方法后追加"Async"后缀生成 Promise 版本(默认后缀见src/promisify.js中的AFTER_PROMISIFIED_SUFFIX),并支持{suffix, filter, promisifier, multiArgs}自定义选项。典型用法见 docs/docs/api/promisification.md:
var fs = require("fs"); Promise.promisifyAll(fs); // 之后即可使用 fs.readFileAsync("file.js", "utf8").then(...)2.5 那什么时候才应该用 Deferred?
官方的回答非常干脆:当你不得不这么做的时候("Well simply, when you have to.")。
只有一种场景是你确实可能需要手动构造的:包裹一个不遵循标准约定的回调 API。比如setTimeout(回调没有 error-first 参数):
//setTimeout that returns a promise function delay(ms) { var deferred = Promise.defer(); // warning, defer is deprecated, use the promise constructor setTimeout(function(){ deferred.fulfill(); }, ms); return deferred.promise; }注意代码中的注释:Promise.defer()已废弃,应优先使用 Promise 构造器。当前仓库的 docs/docs/api/deferred-migration.md 给出了用构造器实现 defer 的标准替代方案:
function defer() { var resolve, reject; var promise = new Promise(function() { resolve = arguments[0]; reject = arguments[1]; }); return { resolve: resolve, reject: reject, promise: promise }; }用构造器改写上面的delay:
function delay(ms) { return new Promise(function(resolve) { setTimeout(resolve, ms); }); }官方文档同时提醒:这类手工包裹应当非常罕见。如果因为"Promise 库无法泛化 promisify 它们"而频繁手写,应当去提交 issue。而如果只是因为无法做静态 promisification(promisify和promisifyAll在运行时反复执行太慢),则应当使用Promise.fromCallback。
Promise.fromCallback(别名Promise.fromNode,详见 docs/docs/api/promise.fromcallback.md)用于"运行时按需 promisification",尤其适合那些不暴露类/原型可供promisifyAll扫描的库:
var Promise = require("bluebird"); // "email-templates" 不暴露可被 promisification 的原型 var emailTemplates = Promise.promisify(require('email-templates')); var templatesDir = path.join(__dirname, 'templates'); emailTemplates(templatesDir).then(function(template) { return Promise.fromCallback(function(callback) { return template("newsletter", callback); }, {multiArgs: true}).spread(function(html, text) { console.log(html, text); }); });{multiArgs: true}在这里是必需的,因为template回调会返回多个成功值(html和text),而 Promise 原生只支持单个成功值;开启后需配合.spread展开数组。
2.6 反模式一自查清单
- 是否在已有 Promise 的地方又创建了 deferred?→ 直接
return已有 Promise; - 是否手写了
new Promise包回调?→ 改用Promise.promisify/promisifyAll; - 是否在运行时反复 promisify 同一个函数?→ 提升到模块顶层执行一次,或改用
Promise.fromCallback; - 手写包装时是否处理了:同步异常、多成功值、
this上下文?→ 这些交给库更稳妥。
三、反模式二:.then(success, fail)反模式
3.1 症状:把 Promise 当成"美化版回调"
第二个反模式几乎可以断定你仍在使用回调思维。原本的写法是:
doThat(function(err, success) { ... });你换成了:
doThat().then(success, err);然后自我安慰"至少代码解耦了"。但官方文档明确指出:.then的双参签名主要是为互操作(interop)设计的,在应用代码中几乎没有理由使用.then(success, fail)。这在同步世界里甚至难以表达——想象同步版本:
var t0; try { t0 = doThat(); } catch(e) { } //deal with t0 here and waste the try-catch var stuff = JSON.parse(t0);同步程序员更可能这样写:
try { var stuff = JSON.parse(doThat()); } catch(e) { }即:让异常自然穿过正常流程,在末尾统一捕获,而不是把正常流程劈成两半。
3.2 正确写法:链式.then+ 尾部.catch
用 Promise 时,请写出与同步版本等价的代码:
doThat() .then(function(v) { return JSON.parse(v); }) .catch(function(e) { });两者的区别在于:.then(success, fail)中success内部抛出的错误不会被同一个fail捕获(它只处理前一个 Promise 的 rejection),而在链式.then(...).catch(...)中,.catch会捕获整条链上任意位置抛出的异常——这正是同步try/catch的语义。
3.3 源码视角:.catch到底是什么
.catch是内建 JavaScript Promise 规范的一部分,本质上是.then(null, function(){})的语法糖。Bluebird 在 src/promise.js 中实现:
Promise.prototype.caught = Promise.prototype["catch"] = function (fn) { ... };同时为兼容早期 ECMAScript 版本提供了别名.caught。
Bluebird 的.catch还额外支持过滤变体(见 docs/docs/api/catch.md 与 src/catch_filter.js),比原生 catch 更安全、更贴近 Java/C# 的多 catch 子句:
somePromise.then(function() { return a.b.c.d(); }).catch(TypeError, function(e) { //TypeError 会到这里,例如访问 undefined 的属性 }).catch(ReferenceError, function(e) { //a 从未声明时会到这里 }).catch(function(e) { //兜底:既不是 TypeError 也不是 ReferenceError });也可以为同一个 handler 指定多个错误类型过滤器:
somePromise.then(function() { return a.b.c.d(); }).catch(TypeError, ReferenceError, function(e) { //编程错误会到这里 }).catch(NetworkError, TimeoutError, function(e) { //日常可预期的网络错误会到这里 }).catch(function(e) { //捕获任何意外错误 });src/catch_filter.js的catchFilter实现表明,Bluebird 支持三类过滤器:
- 错误构造器:要求
item.prototype instanceof Error(实现中先判断item === Error或item.prototype instanceof Error,再e instanceof item); - 谓词函数:接收错误对象作为参数,返回真值则进入该 handler;
- 对象谓词:如
{code: 'ENOENT'},相当于function(e) { return isObject(e) && e.code == 'ENOENT' },使用宽松相等逐属性匹配。
配合手写自定义错误类型(继承Error.prototype),可以实现非常精细的错误分流:
function MyCustomError(message) { this.message = message; this.name = "MyCustomError"; Error.captureStackTrace(this, MyCustomError); } MyCustomError.prototype = Object.create(Error.prototype); MyCustomError.prototype.constructor = MyCustomError; Promise.resolve().then(function() { throw new MyCustomError(); }).catch(MyCustomError, function(e) { //会走到这里 });在.catch中不抛出、也不返回 rejected promise,即为"从失败中恢复",链条会继续:
Promise.reject(Error('fail!')) .catch(function(e) { // fallback with "recover from failure" return Promise.resolve('success!'); // promise or value }) .then(function(result) { console.log(result); // 输出 "success!" });这完全等价于同步代码try { throw Error('fail') } catch(e) { result = 'success!' }。
3.4 反模式二自查清单
- 是否写了
promise.then(success, fail)?→ 改写为promise.then(v => ...).catch(e => ...); success里是否会抛错?→ 用尾部.catch保证能被捕获;- 是否需要按错误类型分流?→ 使用 Bluebird 的过滤式
.catch(TypeError, handler)。
四、小结:把 Promise 当成 Promise 用
两大反模式的共同根源,是把 Promise 当作"带回调的包装纸"。正确的心智模型是:Promise 是同步控制流在异步世界的投影。
| 反模式 | 错误做法 | 正确做法 | 依据 |
|---|---|---|---|
| 显式构造 | 用 deferred 包裹已有 Promise | 直接return已有 Promise,用return传值 | docs/docs/anti-patterns.md、src/thenables.js |
| 显式构造 | 手写new Promise包回调 API | Promise.promisify/promisifyAll/Promise.fromCallback | src/promisify.js、docs/docs/api/promise.fromcallback.md |
| 双参 then | .then(success, fail) | .then(v => ...).catch(e => ...),必要时用过滤式.catch | docs/docs/api/catch.md、src/promise.js、src/catch_filter.js |
关于 Deferred 的使用边界,官方立场是:仅在你不得不手写时使用——比如包裹不遵循 error-first 约定的 API(如setTimeout),且优先用 Promise 构造器而非已废弃的Promise.defer()(迁移方案见 docs/docs/api/deferred-migration.md)。除此之外,让 Bluebird 的 promisification 机制(src/promisify.js)替你完成繁重且易错的工作,把代码写短、把错误通道打通,才是正确的 Promise 打开方式。
- 后端
【免费下载链接】bluebird
:bird: :zap: Bluebird is a full featured promise library with unmatched performance.
相关推荐
Bluebird最佳实践:企业级应用中的Promise模式
Bluebird最佳实践:企业级应用中的Promise模式 Bluebird是一个功能完整的Promise库,专注于创新特性和卓越性能,为企业级JavaScri
后端Sanity Studio 实战 RxJS:掌握官方技能指南中的 RxJS 地道写法与反模式规避
Sanity Studio 实战 RxJS:掌握官方技能指南中的 RxJS 地道写法与反模式规避 Sanity Studio 的核心包( packages/sa
CMS前端context-mode 反模式避坑指南:execute / execute_file 的 8 大常见错误与正确姿势
context mode 反模式避坑指南:execute / execute_file 的 8 大常见错误与正确姿势 本指南以 context mode 官方反
AI 应用MCP 服务AI 插件开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考