Promisify 的源码解析

参考文档

  • 升级bluebird 3后Promise.promisify的函数回调参数问题:3中的使用方法和2还是不一样的
  • How does Bluebird promisify work?:源码讲解promiify的内部机制;
  • Optimizing for V8 - Inlining, Deoptimizations:V8优化相关内容文章
  • Promise.promisify:官方API文档

1. 简述

使用过 Bluebird 的都知道 promisify 这个方法的作用,通过该方法会让 NodeJS 形式的函数风格转换成 Promise 方法,可以认为是一颗 语法糖,例如:

var readFile = Promise.promisify(require("fs").readFile);

readFile("myfile.js", "utf8").then(function(contents) {
    return eval(contents);
}).then(function(result){
    // other code
})

接下来我们就分析一下这个 promisify 的内部流程。下文,我们将以如下的代码片段作为demo来讲解

var Promise = require('bluebird');
var fs = require('fs');

// this is how you read a file without promisify
fs.readFile('/etc/profile', function(err, buffer) {
    console.log('fs.readFile: ' + buffer.toString());
});

// this is the promisified version
var promisifiedRead = Promise.promisify(fs.readFile);
promisifiedRead('/etc/profile')
    .then(function(buffer) {
        console.log('promisified readFile: ' + buffer.toString());
    });

2. 开始剖析

在文件 promisify.js 中:


var makeNodePromisified = canEvaluate
    ? makeNodePromisifiedEval
    : makeNodePromisifiedClosure;

....

function promisify(callback, receiver, multiArgs) {
    return makeNodePromisified(callback, receiver, undefined,
                                callback, null, multiArgs);
}
Promise.promisify = function (fn, options) {
    if (typeof fn !== "function") {
        throw new TypeError("expecting a function but got " + util.classString(fn));
    }
    if (isPromisified(fn)) {
        return fn;
    }
    options = Object(options);
    var receiver = options.context === undefined ? THIS : options.context;
    var multiArgs = !!options.multiArgs;
    var ret = promisify(fn, receiver, multiArgs);
    util.copyDescriptors(fn, ret, propsFilter);
    return ret;
};
  • options 的最基本形式是 {context:this,multiArgs:false}
  • 本质是调用 makeNodePromisifiedEval 或者是 makeNodePromisifiedClosure 方法,根据 canEvaluate 变量选择,该变量是在文件 ./util.js 中定义的,看源码也很快能发现就一句话 var canEvaluate = typeof navigator == "undefined"; navigator 包含有关访问者浏览器的信息,这里主要是区分是否是Node环境;

在 Promise.promisify 官方API文档中有讲过,context就是需要绑定的上下文对象

var redisGet = Promise.promisify(redisClient.get, {context: redisClient});
redisGet('foo').then(function() {
    //...
});

也可以这么写:

var getAsync = Promise.promisify(redisClient.get);
getAsync.call(redisClient, 'foo').then(function() {
    //...
});

multi 的参数可以在 升级bluebird 3后Promise.promisify的函数回调参数问题 中找到示例;

canEvaluate为true表示在Node环境,否则在浏览器环境;首先我们看在浏览器端的实现 makeNodePromisifiedClosure

2.1、makeNodePromisifiedClosure

相应的源代码是:(方便阅读也写上相关的注释)

function makeNodePromisifiedClosure(callback, receiver, _, fn, __, multiArgs) {
    var defaultThis = (function() {return this;})();
    var method = callback;
    if (typeof method === "string") {
        callback = fn;
    }
    function promisified() {
        var _receiver = receiver;
        if (receiver === THIS) _receiver = this;
        var promise = new Promise(INTERNAL);
        
        // _captureStackTrace 方法添加栈跟踪,方便调试; 
        promise._captureStackTrace();
        
        // 获取回调函数的定义:如果是方法名就调用this[method],否则直接调用callback
        var cb = typeof method === "string" && this !== defaultThis
            ? this[method] : callback;
        var fn = nodebackForPromise(promise, multiArgs);
        try {
            cb.apply(_receiver, withAppended(arguments, fn));
        } catch(e) {
            promise._rejectCallback(maybeWrapAsError(e), true, true);
        }
        if (!promise._isFateSealed()) promise._setAsyncGuaranteed();
        return promise;
    }
    util.notEnumerableProp(promisified, "__isPromisified__", true);
    return promisified;
}

这里的 nodebackForPromise 方法相当于工厂函数,你可以想象成是 某种类型的promise生成器,这个名字里的 nodeback 单词是不是很让你莫名奇妙?,不过相信看了源码会让你恍然大悟的,哈哈,我们看一下它的源码(在 ./nodeback.js 文件中)

function nodebackForPromise(promise, multiArgs) {
    return function(err, value) {
        if (promise === null) return;
        if (err) {
            var wrapped = wrapAsOperationalError(maybeWrapAsError(err));
            promise._attachExtraTrace(wrapped);
            promise._reject(wrapped);
        } else if (!multiArgs) {
            promise._fulfill(value);
        } else {
            INLINE_SLICE(args, arguments, 1);
            promise._fulfill(args);
        }
        promise = null;
    };
}

这个方法返回的是一个函数 function(err,value){....},仔细想想,这种风格是不是 node回调方法的风格 ?这不但解释了这也就解释了 nodebackForPromise 名字的来历,也解释了 promisify 方法只能对 node异步函数(比如fs.readFile等)有效;

nodebackForPromise 其中的逻辑就比较简单了,如果有错误就调用promise._reject,成功就调用promise._fulfill,这里也包含了 multiArgs 参数的处理,如果返回多个参数,就把多个参数整合成数组形式;

好了,我们回到主流程,代码执行到 nodebackForPromise 这一行仍然还没有对我们传入的 callback 方法做特殊处理;

直到 cb.apply(_receiver, withAppended(arguments, fn));

这里的withAppended方法定义在 ./util.js中,是一个纯函数,用于拼接数组的,因此withAppended(arguments, fn)仅仅是给现有的入参扩展一个node回调风格的fn

在我们的 demo 里:

var promisifiedRead = Promise.promisify(fs.readFile);
promisifiedRead('/etc/profile')

执行到这里,实质上就是执行 fs.readFile.apply(this,'/etc/profile',fn),是不是就很清晰了,其实和原有的调用方式是一样的!仅仅是在 fn 中加入了promise功能;那么一旦 fs.readFile 执行完成,之后就会调用 fn 方法,也就进入了promise的世界了; 棒棒哒!

2.2、makeNodePromisifiedEval

其实上述解读了 makeNodePromisifiedClosure 方法相信已经了解了 promisify 这种魔法的本质,这节要讲的 makeNodePromisifiedEval 的操作流程也是类似的;

只是因为运行在 node 端,可以 利用V8引擎优化性能,利用其 function inlining 特性,在调用callback 方法时 极大地节约创建闭包的成本

可通过google搜索 v8 函数内联 来查阅更多资料;

内联化对 callback.apply 方法是 不起作用的,除非它调用的是 arguments 参数,而上面我们也看到了,这个参数我们使用 withAppended(arguments, fn),返回的是一个新的参数数组,因此内联优化是不起作用的;

与此相对应的,callback.call方法可以被内联优化;callapply 方法的区别在于,apply接受一个数组作为参数,而call 必须详细指定每一个参数(也正是如此,可以用于内联优化);makeNodePromisifiedEval正是将上述apply方法替换成call方法,以期望达到V8引擎最大的优化性能 —— 因此必须让引擎知道入参个数总数

makeNodePromisifiedEval =
function(callback, receiver, originalName, fn, _, multiArgs) {
    var newParameterCount = Math.max(0, parameterCount(fn) - 1);

    var body = "'use strict';                                  \n\
        var ret = function (Parameters) {                      \n\
            'use strict';                                      \n\
            var len = arguments.length;                        \n\
            var promise = new Promise(INTERNAL);               \n\
            promise._captureStackTrace();                      \n\
            var nodeback = nodebackForPromise(promise, " + multiArgs + ");   \n\
            var ret;                                           \n\
            var callback = tryCatch(fn);                       \n\
            switch(len) {                                      \n\
                [CodeForSwitchCase]                            \n\
            }                                                  \n\
            if (ret === errorObj) {                            \n\
                promise._rejectCallback(maybeWrapAsError(ret.e), true, true);\n\
            }                                                  \n\
            if (!promise._isFateSealed()) promise._setAsyncGuaranteed();                                 \n\
            return promise;                                    \n\
        };                                                     \n\
        notEnumerableProp(ret, '__isPromisified__', true);     \n\
        return ret;                                            \n\
    ".replace("[CodeForSwitchCase]", generateArgumentSwitchCase())
    .replace("Parameters", parameterDeclaration(newParameterCount));

    return new Function("Promise", "fn", "receiver", "withAppended", "maybeWrapAsError", "nodebackForPromise", "tryCatch", "errorObj", "notEnumerableProp", "INTERNAL", body)(Promise, fn, receiver, withAppended, maybeWrapAsError, nodebackForPromise, util.tryCatch, util.errorObj, util.notEnumerableProp, INTERNAL);
};

为了能依据不同的callback构造不同的内联方法,makeNodePromisifiedEval 使用了 原始函数构造器,该函数构造器的参数起于 Promise 终于 INTERNAL;

body变量中就是真正的函数体了,你可以发现其中大部分的代码和 makeNodePromisifiedClosure 方法是一样的,仅仅不一样的是多了一节 CodeForSwitchCase,用于针对不同的入参个数产生不同的 .call 函数调用;

这里的generateArgumentSwitchCase函数比较复杂,这里就不展开了,总之会最后会产生类似如下的代码:

switch(len) {
    case 2:ret = callback.call(this, _arg0, _arg1, nodeback); break;
    case 1:ret = callback.call(this, _arg0, nodeback); break;
    case 0:ret = callback.call(this, nodeback); break;
    case 3:ret = callback.call(this, _arg0, _arg1, _arg2, nodeback); break;

3. 总结

暂无,阅读源码笔记

下面的是我的公众号二维码图片,欢迎关注。

你可能感兴趣的:(javascript,bluebird,源码分析,node.js)