原文地址: http://welefen.com/post/how-to-convert-callback-to-promise.html
前端开发尤其 Node.js 开发中,经常要调用一些异步接口,如:文件操作、网络数据读取。而这些接口默认情况下往往是通过 Callback 方式提供的,即:最后一个参数传入一个回调函数,当出现异常时,将错误信息作为第一个参数传给回调函数,如果正常,第一个参数为 null,后面的参数为对应其他的值。
<!–more–>
var fs = require('fs');
fs.readFile('foo.json', 'utf8', function(err, content) {
if (err) {
//异常情况
} else {
//正常情况
}
})
当这种写法遇上比较复杂的逻辑时,就很容易出现 callback hell 的问题。为此,开发者也积极寻找对应的解决方案,如:Promise、ES6 Generator + co + Promise、ES2016 草案里的 async functions 等。
这几种方案也是慢慢的在进化,视图更好的处理 callback hell 的问题。但这几种方案一致的依赖基础方式都是 Promise,这也是为什么 Promise 并没有引入新的语法但也写进了 ES6 规范的一个大的原因。甚至现在一些新的接口(如:Fetch)直接返回 Promise。
然后对异步接口的处理方式都依赖 Promise,那么下面就来说下如何将 Callback 接口变成 Promise 接口。
Callback 接口变成 Promise 接口
其实 Callback 接口变成 Promise 接口非常简单,包括现在也有很多库都有类似的方法可以转换,如:
- bluebird 模块里有 promisify 方法
- es6-promisify 模块
- ThinkJS 里的 promisify 方法
由于 Callback 接口的参数方式是固定的,所以很容易变成 Promise 接口,如:
let promisify = (fn, receiver) => {
return (...args) => {
return new Promise((resolve, reject) => {
fn.apply(receiver, [...args, (err, res) => {
return err ? reject(err) : resolve(res);
}]);
});
};
};
几行代码基本就搞定了对 Callback 接口对 Promise 的转换,当然上面的代码是用 ES6 代码写的。用 ES5 写的话可以类似下面这样:
var promisify = function promisify(fn, receiver) {
return function () {
for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
return new Promise(function (resolve, reject) {
fn.apply(receiver, [].concat(args, [function (err, res) {
return err ? reject(err) : resolve(res);
}]));
});
};
};
有了 promisify
这样一个函数,那么把 Callback 接口变成 Promise 接口就非常简单了,如:
var fs = require('fs');
var readFilePromise = promisify(fs.readFile, fs); //包装为 Promise 接口
readFilePromise('foo.json', 'utf8').then(function(content){
//正常情况
}).catch(function(err){
//异常情况
})
有了快速转换的方法后,就不用去找模块对应的 Promise 版本的模块了。
特殊情况
有些设计不合理的接口可能会传递多个值给回调函数,如:
var fn = function(foo, callback){
if(success){
callback(null, content1, content2);
}else{
callback(err);
}
}
上面的代码在正常情况下会传递 2 个参数给回调函数,由于 Promise resolve 的时候只能传入一个值,所以这种接口变成 Promise 接口后是无法获取到 content2 数据的。
对于这种情况只能手工来包装了,同时顺便鄙视下设计这个接口的人。
担心性能
有些人担心大量使用 Promise 会引起性能的下降,这个事情在当初 Node.js 设计接口时也争吵了很久,有时候易用性和性能本来就是有些互斥的。
其实可以使用高性能的 Promise 库来提高性能,如:bluebird。简单对比测试发现,blurbird 的性能是 V8 里内置的 Promise 3 倍左右(bluebird 的优化方式见 https://github.com/petkaantonov/bluebird/wiki/Optimization-killers )。
可以通过下面的方式替换调内置的 Promise:
global.Promise = require('bluebird');
如果项目里用了 Babel 编译 ES6 代码的话,可以用下面的方式替换:
//Babel 编译时会把 Promise 编译为 Babel 依赖的 Promise
require('babel-runtime/core-js/promise').default = require('bluebird');
global.Promise = require('bluebird');
mk
学习了!
学习了
来自酷炫的 CNodeMD
自荐下自己的 promise.ify
包, 因为在使用 bluebird 包的时候, 出现了一堆相当主观的 warning. 决定使用原生 Promise.
https://github.com/magicdawn/promise.ify#promiseify
- 对多个值自动使用array包装, 结果去resolve的数组中去取。
- 可以使用 noerr 的情况, 例如 fs.exists, 例如一些前端的无 err callback: $(document).ready(callback);
- 支持指定 this 值, 以及使用运行时 this 值, 同 bluebird
- 支持简单的 promisifyAll, 使用 Object.keys 以及筛选出所有的 method, 进行 promisify
然后就是
- 100% coverage 可依赖哦.
- ES5 Code. 使用 global.Promise
哈哈~想着有一天 ready 变成这样~
const $ = require('jquery');
const pify = require('promise.ify').noerr;
$.fn.readyAsync = pify($.fn.ready);
async function foo(){
await $(document).readyAsync();
return $('#some-id').text();
}
@magicdawn 看了你的代码, 麻烦问下, 在 Promise 的 executor 里面用 try catch 和不用 try catch 有什么不同么? 我写的没加, 不知道有没有什么风险?
// a simple promisify function
function $P(fn) {
return function() {
let THIS = this ? this : {};
let args = [].slice.call(arguments);
return new Promise(function(resolve) {
args.push(function(){
resolve([].slice.call(arguments));
});
fn.apply(this, args);
}.bind(THIS));
};
}
bluebird 的 promisify 可以支持多参的,不需要手工包装:
var P = require('bluebird');
function fn(cb) {
cb(null, 1, 2);
}
P.promisify(fn, { multiArgs: true })().spread((a, b) => console.log(a, b));
@Chunlin-Li 是不需要的,我都准备删掉了,因为在Promise 的实现中会去 catch。加了也是不会出现问题的。
- 没有找到相关规范 https://promisesaplus.com/
- mdn 也没有提 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise
- 但就是可以
> new Promise((resolve, reject) => { throw new Error('boom') })
Promise { <rejected> [Error: boom] }
@magicdawn 呃, 好像是有的.
以及 ES2015 规范中的 25.4.1.1.1 小节的 IfAbruptRejectPromise
好像都是说这个的.
嗯~那个是 then(success, fail) , 在 success 抛出 err 的情况 没有规定 new Promise(executor) executor抛出 err 的情况
@magicdawn
executor 和 thenable 本质不一样么?
我一直以为 executor 就是用来产生 thenable 的 then 方法的.
这不太清楚。。。
mark 自豪地采用 CNodeJS ionic
学习了
回复 马克 From Noder
mark
m
一直不明白要什么要用Promise , 如果是解决嵌套的问题的话 我真心看不出Promise 解决了
@yongningfu 因为 async/await 是基于 Promise