Bluebird Promise 反模式实战指南:显式构造与 `.then(success, fail)` 的正确规避之道
2026/9/20 19:36:42 网站建设 项目流程
  • 后端

【免费下载链接】bluebird

:bird: :zap: Bluebird is a full featured promise library with unmatched performance.

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

导读

本文基于 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.promisifyPromise.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 调用代码(见switchCaseArgumentOrdergenerateArgumentSwitchCase),按函数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] ) -> function
  • nodeFunction需符合 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。而如果只是因为无法做静态 promisificationpromisifypromisifyAll在运行时反复执行太慢),则应当使用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回调会返回多个成功值(htmltext),而 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.jscatchFilter实现表明,Bluebird 支持三类过滤器:

  1. 错误构造器:要求item.prototype instanceof Error(实现中先判断item === Erroritem.prototype instanceof Error,再e instanceof item);
  2. 谓词函数:接收错误对象作为参数,返回真值则进入该 handler;
  3. 对象谓词:如{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包回调 APIPromise.promisify/promisifyAll/Promise.fromCallbacksrc/promisify.js、docs/docs/api/promise.fromcallback.md
双参 then.then(success, fail).then(v => ...).catch(e => ...),必要时用过滤式.catchdocs/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.

项目地址:https://gitcode.com/gh_mirrors/bl/bluebird
点击查看免费下载
上一篇:鸣潮自动化工具完整指南:如何实现后台自动战斗与智能资源收集
下一篇:解决Electron应用模块化难题:RequireJS让代码管理如丝般顺滑

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

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

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

立即咨询