参考文档
- 升级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
方法可以被内联优化;call
和 apply
方法的区别在于,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. 总结
暂无,阅读源码笔记
下面的是我的公众号二维码图片,欢迎关注。