04月19, 2016

如何把 Callback 接口包装成 Promise 接口

原文: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");

本文链接:http://www.75team.com/post/how-to-convert-callback-to-promise.html

-- EOF --

Comments