Promisify 的源码剖析

参考文档

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. 总结

暂无,浏览源码笔记

下面的是我的民众号二维码图片,迎接关注。
《Promisify 的源码剖析》

    原文作者:JSCON简时空
    原文地址: https://segmentfault.com/a/1190000008479644
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞