
Bluebird 回调 API 对接实战使用 promisify、Promise 构造函数与 disposer 将任意回调代码 Promise 化【免费下载链接】bluebird:bird: :zap: Bluebird is a full featured promise library with unmatched performance.项目地址: https://gitcode.com/gh_mirrors/bl/bluebird导读本文基于 Bluebird 官方文档《Working with Callbacks》系统讲解如何将 Node.js 中常见的 error-first 回调 API、一次性事件、延迟定时器、浏览器非标准 API 以及数据库驱动等既有回调代码安全高效地转换为 Promise 接口。你将掌握Promise.promisify/Promise.promisifyAll的完整用法与底层原理动态重编译、filter/multiArgs/suffix 等选项、用new Promise手工封装事件与自定义 API 的规范写法以及借助Promise.using disposer 实现数据库连接自动释放的资源管理模式并配套可复制的 PostgreSQL、MySQL、Redis、MongoDB、Mongoose 等实战示例。背景为什么需要把回调 API 转成 Promise在开始之前先统一两个基础概念。Promise 具有状态开始时处于pending待定最终会 settle落定为fulfilled已兑现计算成功完成rejected已拒绝计算失败。一个关键纪律是返回 Promise 的函数永远不应该 throw。它应该总是成功返回一个 Promise出错时以 rejected 状态表达。如果从返回 Promise 的函数里直接 throw调用者就不得不同时写try/catch和.catch两套错误处理而使用 promisified API 的人并不会预期 Promise 会抛出同步异常。因此将既有回调风格 API 转成 Promise 风格不仅是语法上的美化更是为了获得 Promise 体系提供的throw 安全throw safety、统一错误处理与链式组合能力。Bluebird 官方指南给出的结论是这种转换不仅容易而且很快。自动转换 vs 手动转换把回调 API 转换成 Promise API 有两种主要方式手动转换手工把每个 API 调用映射为返回 Promise 的函数自动转换让 Bluebird 替你完成通过Promise.promisify与Promise.promisifyAll。官方文档强烈推荐后者。原因有两点Promise 提供了 throw 安全等强大保证手动转换时很难完整复现这些保证自动转换在实现上使用了**动态重编译dynamic recompilation**技术生成极快的高性能包装器引入的开销非常小。从源码看Promise.promisify在 Node 环境下优先走makeNodePromisifiedEval路径见 src/promisify.js它根据原函数fn.length推断参数个数用new Function现场生成带switch(len)分派的最优调用代码只对参数个数附近若干种情况逐一生成调用分支超出范围才退回apply兜底。这正是官方所称动态重编译、极低开销的实现依据。当无法使用eval如浏览器环境时则退化为makeNodePromisifiedClosure闭包实现src/promisify.js。使用 Node 约定处理回调 APINode/io.js 中的大多数 API 遵循error-first单值参数error-first, single-parameter回调约定function getStuff(data, callback) { // ... } getStuff(dataParam, function(err, data) { if (!err) { // 成功分支 } });Node 核心模块的绝大多数 API 都属于这一类而 Bluebird 提供了两种快速高效的转换工具Promise.promisify转换单个回调函数返回一个返回 Promise 的新函数不修改原函数Promise.promisifyAll接收一个充满函数的对象为其中每个函数生成带Async后缀默认的新函数不改动原函数只新增方法。注意完整参数与更多用法示例请查阅上述两个 API 文档页面。以fs.readFile为例回调写法var fs require(fs); fs.readFile(name, utf8, function(err, data) { // ... });Promise 写法var fs Promise.promisifyAll(require(fs)); fs.readFileAsync(name, utf8).then(function(data) { // ... });注意新方法带有Async后缀即fs.readFileAsync它没有替换原来的fs.readFile。单个函数也可以被 promisify例如var request Promise.promisify(require(request)); request(foo.bar).then(function(result) { // ... });Promise.promisify 的签名与选项Promise.promisify( function(any arguments..., function callback) nodeFunction, [Object { multiArgs: booleanfalse, context: anythis } options] ) - function关键行为依据 docs/docs/api/promise.promisify.md返回一个包装nodeFunction的函数不再接收回调而是返回一个 Promise其命运由原函数的回调行为决定node 函数须符合回调作为最后一个参数、err 作为第一个参数、成功值作为第二个参数的约定若回调被以多个成功值调用默认只取第一个作为 fulfillment 值设置multiArgs: true后结果 Promise 会始终以成功值数组fulfill。因为 Promise 只支持单一成功值而部分回调 API 会传出多个成功值传入context时nodeFunction将以context作为this被调用。完整示例promisifyfs.readFile并配合错误过滤var readFile Promise.promisify(require(fs).readFile); readFile(myfile.js, utf8).then(function(contents) { return eval(contents); }).then(function(result) { console.log(The result of evaluating myfile.js, result); }).catch(SyntaxError, function(e) { console.log(File had syntax error, e); // 捕获其他任何错误 }).catch(function(e) { console.log(Error reading file, e); });如果 node 函数是某个对象的方法可以通过context传入接收者var redisGet Promise.promisify(redisClient.get, {context: redisClient}); redisGet(foo).then(function() { // ... });也可以不传context之后用.call指定接收者var getAsync Promise.promisify(redisClient.get); getAsync.call(redisClient, foo).then(function() { // ... });Promise.promisify的底层行为见 src/promisify.js包括非函数入参抛TypeError若函数已被 promisify带有__isPromisified__标记则直接返回原样multiArgs通过!!options.multiArgs强制布尔化。Promise.promisifyAll 的签名与选项Promise.promisifyAll( Object target, [Object { suffix: StringAsync, multiArgs: booleanfalse, filter: boolean function(String name, function func, Object target, boolean passesDefaultFilter), promisifier: function(function originalFunction, function defaultPromisifier) } options] ) - ObjectpromisifyAll会遍历对象的属性包括原型链为每个函数创建带suffix默认Async后缀的异步版本并返回原输入对象。对象的类属性即很多模块主导出那种函数值带非空.prototype的属性也会被处理静态方法与实例方法都会被 promisify。关键行为要点依据 docs/docs/api/promise.promisifyall.md只考虑可枚举属性若对象已有某方法的 promisified 版本则跳过目标方法须符合 Node 回调约定若回调传出多个成功值fulfillment 值为它们的数组若某个方法名已带Async后缀会抛出异常源码中的checkValid会做冲突检测见 src/promisify.jssuffix 注意事项须小心选择避免冲突、建议使用 PascalCase、必须是合法 JavaScript 标识符ASCII 字母并且整个应用内应统一使用同一后缀可通过包装函数固化module.exports function myPromisifyAll(target) { return Promise.promisifyAll(target, {suffix: MySuffix}); };示例redis 与 fsPromise.promisifyAll(require(redis)); // 之后所有 redis 客户端实例都有返回 Promise 的方法 redisClient.hexistsAsync(myhash, field).then(function(v) { // ... }).catch(function(e) { // ... });var fs Promise.promisifyAll(require(fs)); fs.readFileAsync(myfile.js, utf8).then(function(contents) { console.log(contents); }).catch(function(e) { console.error(e.stack); });multiArgs 选项当某个模块只有个别方法是多参数回调时可以先用filter只 promisify 那一个方法并开启multiArgs再对剩余方法进行常规 promisifyPromise.promisifyAll(something, { filter: function(name) { return name theMultiArgMethodIwant; }, multiArgs: true }); // 剩余方法 Promise.promisifyAll(something);filter 选项Promise.promisifyAll(..., { filter: function(name, func, target, passesDefaultFilter) { // name 待 promisify 的属性名不含后缀 // func 该函数 // target 目标对象promisified 函数将以 name suffix 挂在其上 // passesDefaultFilter 默认过滤器是否放行 // 返回 boolean返回值会被强制转换不返回任何值等同于返回 false return passesDefaultFilter ... } });默认过滤器会忽略以下划线开头的属性、不是合法 JavaScript 标识符的属性以及构造函数即.prototype上带有可枚举属性的函数。默认过滤逻辑可参见 src/promisify.js 的defaultFilter。promisifier 选项自定义 promisifier 可以处理那些不遵循 Node 约定的 API例如 Chrome 扩展中的 chrome APIs。promisifier 接收原始方法引用返回一个返回 Promise 的函数function DOMPromisifier(originalMethod) { // 返回一个函数 return function promisified() { var args [].slice.call(arguments); // 保证原始方法以正确的接收者被调用 var self this; // 该函数返回一个 Promise return new Promise(function(resolve, reject) { args.push(resolve, reject); originalMethod.apply(self, args); }); }; } // Promisify 例如 chrome.browserAction Promise.promisifyAll(chrome.browserAction, {promisifier: DOMPromisifier}); // 之后 chrome.browserAction.getTitleAsync({tabId: 1}) .then(function(result) { // ... });filter与promisifier组合使用的经典案例restler 事件发射器风格 APIvar Promise require(bluebird); var restler require(restler); var methodNamesToPromisify get post put del head patch json postJson putJson.split( ); function EventEmitterPromisifier(originalMethod) { // 返回一个函数 return function promisified() { var args [].slice.call(arguments); // 保证原始方法以正确的接收者被调用 var self this; // 该函数返回一个 Promise return new Promise(function(resolve, reject) { // 在此调用 originalMethod若它抛出会以抛出的错误拒绝返回的 Promise var emitter originalMethod.apply(self, args); emitter .on(success, function(data, response) { resolve([data, response]); }) .on(fail, function(data, response) { // 错误响应如 400 resolve([data, response]); }) .on(error, function(err) { reject(err); }) .on(abort, function() { reject(new Promise.CancellationError()); }) .on(timeout, function() { reject(new Promise.TimeoutError()); }); }); }; } Promise.promisifyAll(restler, { filter: function(name) { return methodNamesToPromisify.indexOf(name) -1; }, promisifier: EventEmitterPromisifier }); // 之后在另一个文件中 var restler require(restler); restler.getAsync(http://..., ...,).spread(function(data, response) { // ... });利用defaultPromisifier参数在标准 promisification 之上增强例如让 promisified 函数自动等待作为参数的 Promisevar fs Promise.promisifyAll(require(fs), { promisifier: function(originalFunction, defaultPromisifer) { var promisified defaultPromisifier(originalFunction); return function() { // 增强标准 promisification支持把 Promise 作为参数 var args [].slice.call(arguments); var self this; return Promise.all(args).then(function(awaitedArgs) { return promisified.apply(self, awaitedArgs); }); }; } }); // 所有 promisified fs 函数现在会先等待参数中的 Promise 兑现 var version fs.readFileAsync(package.json, utf8).then(JSON.parse).get(version); fs.writeFileAsync(the-version.txt, version, utf8);一次 promisify 多个类把多个类放进一个数组再传给promisifyAll可一次性完成var Pool require(mysql/lib/Pool); var Connection require(mysql/lib/Connection); Promise.promisifyAll([Pool, Connection]);原理数组在此充当模块其下标就是模块的类属性。源码中Promise.promisifyAll会遍历inheritedDataKeys对isClass(value)的类同时处理其.prototype与自身src/promisify.js因此类数组也能被整体识别并逐个类完成静态/实例方法 promisify。重要提示Promise.promisify与Promise.promisifyAll依赖动态重编译来生成极快的包装器因此每个函数/对象应只调用一次在模块加载时执行。在无法这样预生成包装器的场景使用 Promise.fromCallback 按需转换。底层nodeback 如何决定 Promise 命运无论哪种 promisify 方式最终都会创建一个 node 风格的 nodeback 回调交给原函数。其核心逻辑在 src/nodeback.js回调收到err时将错误包装为 OperationalError对未类型化 Error并调用promise._reject(wrapped)拒绝 Promise未传err且multiArgs为 false 时以第一个成功值promise._fulfill(value)multiArgs为 true 时把arguments从下标 1 开始的所有成功值切片成数组后 fulfill。这解释了为什么multiArgs: true时总是得到成功值数组。一次性事件用 new Promise 封装有时我们只想知道某个一次性事件何时完成——例如一个 stream 结束。此时可以使用 new Promise。注意这个方案只在无法进行自动转换时才考虑。Promise 建模的是随时间推移的单个值它只会 resolve一次——所以它非常适合单个事件但不推荐用于多事件 API。例如绑定 window 的onload事件可以在窗口加载完成时 resolve// onload 示例Promise 构造函数接收一个 resolver 函数 // 由它告诉 Promise 何时 resolve 并触发其 then 处理器。 var loaded new Promise(function(resolve, reject) { window.addEventListener(load, resolve); }); loaded.then(function() { // 这里 window 已加载完成 });另一个连接就绪的例子。下面的初版写法有缺陷我们马上解释原因function connect() { var connection myConnector.getConnection(); // 同步调用。 return new Promise(function(resolve, reject) { connection.on(ready, function() { // 连接建立后将 promise 标记为 fulfilled。 resolve(connection); }); connection.on(error, function(e) { // 若连接失败将其标记为 rejected。 reject(e); // e 最好是 Error 实例。 }); }); }问题在于getConnection本身可能因为某些原因 throw一旦抛出就会产生同步拒绝synchronous rejection。异步操作应当始终保持异步以避免双重防护和竞态条件race conditions。因此最好把同步部分也放进 Promise 构造函数内部function connect() { return new Promise(function(resolve, reject) { // 如果 getConnection 在这里抛出得到的将不是异常而是拒绝 // 从而产生一个更加一致的 API。 var connection myConnector.getConnection(); connection.on(ready, function() { // 连接建立后将 promise 标记为 fulfilled。 resolve(connection); }); connection.on(error, function(e) { // 若连接失败将其标记为 rejected。 reject(e); // e 最好是 Error 实例 }); }); }这也是 Bluebird Promise 构造函数resolver 内 throw 安全保证的实际体现放在构造函数内的代码即使抛异常也会被捕获并转换为拒绝。处理延迟直接用 Promise.delay时间延迟/定时器不需要转换成 Bluebird API——Bluebird 已经内置 Promise.delayPromise.delay( int ms, [any|Promiseany valueundefined] ) - Promise返回一个在ms毫秒后以value或undefined兑现的 Promise若value本身是 Promise则倒计时会在它兑现后才开始且返回的 Promise 以value的兑现值兑现若value是已拒绝的 Promise结果 Promise 会立即被拒绝。Promise.delay(500).then(function() { console.log(500 ms passed); return Hello world; }).delay(500).then(function(helloWorldString) { console.log(helloWorldString); console.log(another 500 ms passed) ; });更多用法与示例请参考 timers 文档。处理浏览器 API浏览器 API 往往不符合统一约定自动 promisification 对它们经常会失败。如果遇到 promisify 和 promisifyAll 都无法处理的 API请参考下一节处理任意其他 API用手动方式封装此外前面提到的自定义promisifier如 Chrome 扩展 API 的DOMPromisifier也是浏览器场景的重要工具。处理数据库Promise.using 与 disposer对于资源管理尤其是数据库场景Bluebird 内置了强大的 Promise.using 与 disposer 体系。它类似于 Python 的with、C# 的using、Java 的 try/resource 或 C 的 RAII让你以自动化的方式处理资源生命周期。Promise.prototype.disposer(fn)会把当前 Promise 变成 disposer见 src/using.js而Promise.using会等待所有资源 Promise 兑现执行用户函数并在**最后lastly**无论成功失败都依次调用每个资源的释放函数见 src/using.js 的dispose迭代器与 src/using.js 的 lastly 挂钩。Promise.using至少需要 2 个参数最后一个参数必须是函数src/using.js。更多示例请参阅 Promise.using 文档。Mongoose / MongoDBMongoose 使用持久连接驱动自身负责重连/释放因此不需要用using管理——只需在服务启动时连接然后用 promisification 暴露 Promise 接口。注意Mongoose 虽然自带 Promise 支持但其 Promise 明显更慢且不报告未处理拒绝unhandled rejections因此仍然推荐对它使用自动 promisificationvar Mongoose Promise.promisifyAll(require(mongoose));Sequelize / RethinkDB / BookshelfSequelize内部已经使用 Bluebird Promise提供返回 Promise 的 API直接用即可RethinkDB内部已经使用 Bluebird Promise提供返回 Promise 的 API直接用即可Bookshelf内部已经使用 Bluebird Promise提供返回 Promise 的 API直接用即可。PostgreSQL为驱动创建 disposervar pg require(pg); // 若 pg 尚未被正确 promisify请取消注释 //var Promise require(bluebird); //Promise.promisifyAll(pg, { // filter: function(methodName) { // return methodName connect // }, // multiArgs: true //}); // 正常 promisify 其余部分 //Promise.promisifyAll(pg); function getSqlConnection(connectionString) { var close; return pg.connectAsync(connectionString).spread(function(client, done) { close done; return client; }).disposer(function() { if (close) close(); }); } module.exports getSqlConnection;使用方式var using Promise.using; using(getSqlConnection(), function(conn) { // 在这里使用连接并 _返回 promise_ }).then(function(result) { // 连接在这里已经释放 });这里pg.connect的回调会返回(client, done)两个成功值因此先用filtermultiArgs: true对connect单独 promisify再用.spread拆开最后.disposer负责在结束时调用done()归还连接。也可以使用 disposer 模式并非真正的.disposer做事务管理function withTransaction(fn) { return Promise.using(pool.acquireConnection(), function(connection) { var tx connection.beginTransaction() return Promise .try(fn, tx) .then(function(res) { return connection.commit().thenReturn(res) }, function(err) { return connection.rollback() .catch(function(e) {/* 也许可以把 rollback 错误合并进 err */}) .thenThrow(err); }); }); } exports.withTransaction withTransaction;调用方式withTransaction(tx { return tx.queryAsync(...).then(function() { return tx.queryAsync(...) }).then(function() { return tx.queryAsync(...) }); });MySQL为驱动创建 disposervar mysql require(mysql); // 若 mysql 尚未被正确 promisify请取消注释 // var Promise require(bluebird); // Promise.promisifyAll(mysql); // Promise.promisifyAll(require(mysql/lib/Connection).prototype); // Promise.promisifyAll(require(mysql/lib/Pool).prototype); var pool mysql.createPool({ connectionLimit: 10, host: example.org, user: bob, password: secret }); function getSqlConnection() { return pool.getConnectionAsync().disposer(function(connection) { connection.release(); }); } module.exports getSqlConnection;用法与 PostgreSQL 示例类似。同样可以使用 disposer 模式而非真正的.disposer做事务管理参照上面的 PostgreSQL 示例。更多常见库的 promisify 速查下面是上述实践在常见库上的速查注意由于Promise.promisify/promisifyAll使用动态重编译生成极快包装器请把调用放在模块加载期只执行一次// 最流行的 redis 模块 var Promise require(bluebird); Promise.promisifyAll(require(redis));// 最流行的 mongodb 模块 var Promise require(bluebird); Promise.promisifyAll(require(mongodb));// 最流行的 mysql 模块 var Promise require(bluebird); // 注意该库的类并不是主导出的属性 // 所以需要手动 require 并分别 promisifyAll Promise.promisifyAll(require(mysql/lib/Connection).prototype); Promise.promisifyAll(require(mysql/lib/Pool).prototype);// Mongoose var Promise require(bluebird); Promise.promisifyAll(require(mongoose));// Request var Promise require(bluebird); Promise.promisifyAll(require(request)); // 请使用 request.getAsync(...) 而不是 request(..)后者不会返回 Promise// mkdir var Promise require(bluebird); Promise.promisifyAll(require(mkdirp)); // 请使用 mkdirp.mkdirpAsync 而不是 mkdirp(..)后者不会返回 Promise// winston var Promise require(bluebird); Promise.promisifyAll(require(winston));// rimraf var Promise require(bluebird); // 该模块没有整体 promisify但返回的函数被 promisify 了 var rimrafAsync Promise.promisify(require(rimraf));// xml2js var Promise require(bluebird); Promise.promisifyAll(require(xml2js));// jsdom var Promise require(bluebird); Promise.promisifyAll(require(jsdom));// fs-extra var Promise require(bluebird); Promise.promisifyAll(require(fs-extra));// prompt var Promise require(bluebird); Promise.promisifyAll(require(prompt));// Nodemailer var Promise require(bluebird); Promise.promisifyAll(require(nodemailer));// ncp var Promise require(bluebird); Promise.promisifyAll(require(ncp));// pg var Promise require(bluebird); Promise.promisifyAll(require(pg));以上所有库都以某种方式暴露了它们的类。如果某个库不暴露类例如只提供工厂函数仍可通过创建一个一次性实例来 promisifyvar ParanoidLib require(...); var throwAwayInstance ParanoidLib.createInstance(); Promise.promisifyAll(Object.getPrototypeOf(throwAwayInstance)); // 从此以后所有新实例——甚至 throwAwayInstance 本身——都突然支持 Promise 了原理可参考 src/promisify.jspromisifiableMethods使用inheritedDataKeys沿原型链收集函数过滤掉已被 promisify__isPromisified__或已有同名加后缀方法者再统一生成包装器。对应的测试覆盖见 test/mocha/promisify.js。处理任意其他 API有时你不得不面对一些不符合统一约定的、不一致的 API。注意返回 Promise 的函数永远不应 throw。例如一个同时提供onLoad与onFail两个回调的 APIfunction getUserData(userId, onLoad, onFail) { ... }可以用 Promise 构造函数把它转换为返回 Promise 的函数function getUserDataAsync(userId) { return new Promise(function(resolve, reject) { // 把所有代码放在这里此区域是 throw 安全的。 getUserData(userId, resolve, reject); }); }当 API 既不是 Node error-first 约定、又不能整体 promisify 时这是最直接的兜底方案如果它只是错误优先但发生在调用现场也可以考虑 Promise.fromCallback——它接收一个 node 风格 resolver返回 Promise适合库不暴露类、无法预先自动 promisify 的即时转换场景。结语本指南覆盖了把回调代码迁移到 Bluebird Promise 的完整路径优先使用Promise.promisify/Promise.promisifyAll自动转换配合suffix、filter、multiArgs、promisifier选项应对各种形态的 API对一次性事件用new Promise封装并保持异步一致性延迟直接用Promise.delay数据库场景用Promise.using disposer 自动管理连接最后是万能的手动封装兜底。所有关键行为多成功值处理、throw 安全、资源释放时机都能在 src/promisify.js、src/nodeback.js 与 src/using.js 的源码中得到印证。按此路线迁移你将获得一致、可组合且不牺牲性能的 Promise 代码。【免费下载链接】bluebird:bird: :zap: Bluebird is a full featured promise library with unmatched performance.项目地址: https://gitcode.com/gh_mirrors/bl/bluebird创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考