` 的正确规避之道)
后端【免费下载链接】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.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.mdPromise.promisify( function(any arguments..., function callback) nodeFunction, [Object { multiArgs: booleanfalse, context: anythis } 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.mdvar 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 }; }用构造器改写上面的delayfunction delay(ms) { return new Promise(function(resolve) { setTimeout(resolve, ms); }); }官方文档同时提醒这类手工包裹应当非常罕见。如果因为Promise 库无法泛化 promisify 它们而频繁手写应当去提交 issue。而如果只是因为无法做静态 promisificationpromisify和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包回调 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点击查看免费下载相关推荐Bluebird最佳实践企业级应用中的Promise模式Bluebird最佳实践企业级应用中的Promise模式 Bluebird是一个功能完整的Promise库专注于创新特性和卓越性能为企业级JavaScri后端Sanity Studio 实战 RxJS掌握官方技能指南中的 RxJS 地道写法与反模式规避Sanity Studio 实战 RxJS掌握官方技能指南中的 RxJS 地道写法与反模式规避 Sanity Studio 的核心包 packages/saCMS前端context-mode 反模式避坑指南execute / execute_file 的 8 大常见错误与正确姿势context mode 反模式避坑指南execute / execute_file 的 8 大常见错误与正确姿势 本指南以 context mode 官方反AI 应用MCP 服务AI 插件开发工具上一篇鸣潮自动化工具完整指南如何实现后台自动战斗与智能资源收集下一篇解决Electron应用模块化难题RequireJS让代码管理如丝般顺滑创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考