精华 如何把 Callback 接口包装成 Promise 接口
发布于 8 天前 作者 welefen 505 次浏览 最后一次编辑是 7 天前 来自 分享

原文地址: http://welefen.com/post/how-to-convert-callback-to-promise.html

前端开发尤其 Node.js 开发中,经常要调用一些异步接口,如:文件操作、网络数据读取。而这些接口默认情况下往往是通过 Callback 方式提供的,即:最后一个参数传入一个回调函数,当出现异常时,将错误信息作为第一个参数传给回调函数,如果正常,第一个参数为 null,后面的参数为对应其他的值。

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');
13 回复

学习了

来自酷炫的 CNodeMD

自荐下自己的 promise.ify 包, 因为在使用 bluebird 包的时候, 出现了一堆相当主观的 warning. 决定使用原生 Promise. https://github.com/magicdawn/promise.ify#promiseify

  1. 对多个值自动使用array包装, 结果去resolve的数组中去取。
  2. 可以使用 noerr 的情况, 例如 fs.exists, 例如一些前端的无 err callback: $(document).ready(callback);
  3. 支持指定 this 值, 以及使用运行时 this 值, 同 bluebird
  4. 支持简单的 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。加了也是不会出现问题的。

@Chunlin-Li

> new Promise((resolve, reject) => { throw new Error('boom') })
Promise { <rejected> [Error: boom] }

@magicdawn 呃, 好像是有的.

Promise/A+ point 60

以及 ES2015 规范中的 25.4.1.1.1 小节的 IfAbruptRejectPromise 好像都是说这个的.

@Chunlin-Li

嗯~那个是 then(success, fail) , 在 success 抛出 err 的情况 没有规定 new Promise(executor) executor抛出 err 的情况

@magicdawn executor 和 thenable 本质不一样么?
我一直以为 executor 就是用来产生 thenable 的 then 方法的.

@Chunlin-Li

这不太清楚。。。

回到顶部