资讯专栏INFORMATION COLUMN

Promisify 的源码解析

gougoujiang / 330人阅读

摘要:参考文档升级后的函数回调参数问题中的使用方法和还是不一样的源码讲解的内部机制优化相关内容文章官方文档简述使用过的都知道这个方法的作用,通过该方法会让形式的函数风格转换成方法,可以认为是一颗语法糖,例如接下来我们就分析一下这个的内部流程。

参考文档

升级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";                                  

        var ret = function (Parameters) {                      

            "use strict";                                      

            var len = arguments.length;                        

            var promise = new Promise(INTERNAL);               

            promise._captureStackTrace();                      

            var nodeback = nodebackForPromise(promise, " + multiArgs + ");   

            var ret;                                           

            var callback = tryCatch(fn);                       

            switch(len) {                                      

                [CodeForSwitchCase]                            

            }                                                  

            if (ret === errorObj) {                            

                promise._rejectCallback(maybeWrapAsError(ret.e), true, true);

            }                                                  

            if (!promise._isFateSealed()) promise._setAsyncGuaranteed();                                 

            return promise;                                    

        };                                                     

        notEnumerableProp(ret, "__isPromisified__", true);     

        return ret;                                            

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

暂无,阅读源码笔记

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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/81757.html

相关文章

  • 现代JS中流程控制:详解Callbacks 、Promises 、Async/Await

    摘要:控制台将显示回调地狱通常,回调只能由一个异步函数调用。更多资源使更友好规范使用异步函数简化异步编码旅程异步编程是一项在中无法避免的挑战。 JavaScript经常声称是_异步_。那是什么意思?它如何影响发展?近年来这种方法有何变化? 请思考以下代码: result1 = doSomething1(); result2 = doSomething2(result1); 大多数语言都处理每...

    shadowbook 评论0 收藏0
  • 现代JS中流程控制:详解Callbacks 、Promises 、Async/Await

    摘要:控制台将显示回调地狱通常,回调只能由一个异步函数调用。更多资源使更友好规范使用异步函数简化异步编码旅程异步编程是一项在中无法避免的挑战。 JavaScript经常声称是_异步_。那是什么意思?它如何影响发展?近年来这种方法有何变化? 请思考以下代码: result1 = doSomething1(); result2 = doSomething2(result1); 大多数语言都处理每...

    oujie 评论0 收藏0
  • 现代JS中流程控制:详解Callbacks 、Promises 、Async/Await

    摘要:控制台将显示回调地狱通常,回调只能由一个异步函数调用。更多资源使更友好规范使用异步函数简化异步编码旅程异步编程是一项在中无法避免的挑战。 JavaScript经常声称是_异步_。那是什么意思?它如何影响发展?近年来这种方法有何变化? 请思考以下代码: result1 = doSomething1(); result2 = doSomething2(result1); 大多数语言都处理每...

    anquan 评论0 收藏0
  • util.promisify 那些事儿

    摘要:自定义的化有那么一些场景,是不能够直接使用来进行转换的,有大概这么两种情况没有遵循约定的回调函数返回多个参数的回调函数首先是第一个,如果没有遵循我们的约定,很可能导致的误判,得不到正确的反馈。 util.promisify是在node.js 8.x版本中新增的一个工具,用于将老式的Error first callback转换为Promise对象,让老项目改造变得更为轻松。 在官方推...

    shuibo 评论0 收藏0
  • [译] Node.js 8: util.promisify()

    摘要:例如,的回调函数包含下面几个参数转换成之后,它的参数将会变成这样一个对象通过内部符号处理非标准回调函数。 Nodejs 8 有一个新的工具函数 util.promisify()。他将一个接收回调函数参数的函数转换成一个返回Promise的函数。 1、util.promisify()小例子 如果你给以下命令传入文件路径,则会输出文件内容 // echo.js const {promis...

    Shimmer 评论0 收藏0

发表评论

0条评论

gougoujiang

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<