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

1920次阅读  |  发布于5年以前

前端开发尤其 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 接口非常简单,包括现在也有很多库都有类似的方法可以转换,如:

由于 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");

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8