Nodejs util 模块提供了不少工具函数。为了解决回调地狱问题,Nodejs v8.0.0 提供了 promisify 方法能够将 Callback 转为 Promise 对象。node
工做中对于一些老项目,有 callback 的一般也会使用 util.promisify 进行转换,以前更可能是知其然不知其因此然,本文会从基本使用和对源码的理解实现一个相似的函数功能。git
在介绍 util.promisify 的基础使用以后,实现一个自定义的 util.promisify 函数的简单版本。github
将 callback 转为 promise 对象,首先要确保这个 callback 为一个错误优先的回调函数,即 (err, value) => ... err 指定一个错误参数,value 为返回值。数组
如下将以 fs.readFile 为例进行介绍。promise
建立一个 text.txt 文件bash
建立一个 text.txt 文件,写入一些自定义内容,下面的 Demo 中咱们会使用 fs.readFile 来读取这个文件进行测试。函数
// text.txt
Nodejs Callback 转 Promise 对象测试
复制代码
传统的 Callback 写法工具
const util = require('util');
fs.readFile('text.txt', 'utf8', function(err, result) {
console.error('Error: ', err);
console.log('Result: ', result); // Nodejs Callback 转 Promise 对象测试
});
复制代码
Promise 写法测试
这里咱们使用 util.promisify 将 fs.readFile 转为 Promise 对象,以后咱们能够进行 .then、.catch 获取相应结果ui
const { promisify } = require('util');
const readFilePromisify = util.promisify(fs.readFile); // 转化为 promise
readFilePromisify('text.txt', 'utf8')
.then(result => console.log(result)) // Nodejs Callback 转 Promise 对象测试
.catch(err => console.log(err));
复制代码
自定义 mayJunPromisify 函数实现 callback 转换为 promise,核心实现以下:
function mayJunPromisify(original) {
if (typeof original !== 'function') { // {1} 校验
throw new Error('The "original" argument must be of type Function. Received type undefined')
}
function fn(...args) { // {2}
return new Promise((resolve, reject) => {
try {
// original 例如,fs.readFile.call(this, 'filename', 'utf8', (err, result) => ...)
original.call(this, ...args, (err, result) => {
if (err) {
reject(err);
} else {
resolve(result);
}
});
} catch(err) {
reject(err);
}
});
}
return fn; // {3}
}
复制代码
如今使用咱们自定义的 mayJunPromisify 函数作一个测试
const readFilePromisify = mayJunPromisify(fs.readFile);
readFilePromisify('text.txt', 'utf8')
.then(result => console.log(result)) // Nodejs Callback 转 Promise 对象测试
.catch(err => console.log(err));
复制代码
另外一个功能是可使用 util.promisify.custom 符号重写 util.promisify 返回值。
在 fs.readFile 上定义 util.promisify.custom 符号,其功能为禁止读取文件。
注意顺序要在 util.promisify 以前。
fs.readFile[util.promisify.custom] = () => {
return Promise.reject('该文件暂时禁止读取');
}
const readFilePromisify = util.promisify(fs.readFile);
readFilePromisify('text.txt', 'utf8')
.then(result => console.log(result))
.catch(err => console.log(err)); // 该文件暂时禁止读取
复制代码
// 因此说 util.promisify.custom 是一个符号
const kCustomPromisifiedSymbol = Symbol('util.promisify.custom');
mayJunPromisify.custom = kCustomPromisifiedSymbol;
function mayJunPromisify(original) {
if (typeof original !== 'function') {
throw new Error('The "original" argument must be of type Function. Received type undefined')
}
// 变更之处 -> start
if (original[kCustomPromisifiedSymbol]) { // {1}
const fn = original[kCustomPromisifiedSymbol];
if (typeof fn !== 'function') { // {2}
throw new Error('The "mayJunPromisify.custom" property must be of type Function. Received type number');
}
// {3}
return Object.defineProperty(fn, kCustomPromisifiedSymbol, {
value: fn, enumerable: false, writable: false, configurable: true
});
}
// end <- 变更之处
function fn(...args) {
...
}
return fn;
}
复制代码
一样测试下咱们自定义的 mayJunPromisify.custom 函数。
fs.readFile[mayJunPromisify.custom] = () => {
return Promise.reject('该文件暂时禁止读取');
}
const readFilePromisify = mayJunPromisify(fs.readFile);
readFilePromisify('text.txt', 'utf8')
.then(result => console.log(result))
.catch(err => console.log(err)); // 该文件暂时禁止读取
复制代码
一般状况下咱们是 (err, value) => ... 这种方式实现的,结果只有 value 一个参数,可是呢有些例外状况,例如 dns.lookup 它的回调形式是 (err, address, family) => ... 拥有三个参数,一样咱们也要对这种状况作兼容。
和上面区别的地方在于 .then 接收到的是一个对象 { address, family } 先明白它的基本使用,下面会展开具体是怎么实现的
const dns = require('dns');
const lookupPromisify = util.promisify(dns.lookup);
lookupPromisify('nodejs.red')
.then(({ address, family }) => {
console.log('地址: %j 地址族: IPv%s', address, family);
})
.catch(err => console.log(err));
复制代码
相似 dns.lookup 这样的函数在回调(Callback)时提供了多个参数列表。
为了支持 util.promisify 也都会在函数上定义一个 customPromisifyArgs 参数,value 为回调时的多个参数名称,类型为数组,例如 dns.lookup 绑定的 customPromisifyArgs 的 value 则为 ['address', 'family'],其主要目的也是为了适配 util.promisify。
dns.lookup 支持 util.promisify 核心实现
// https://github.com/nodejs/node/blob/v12.x/lib/dns.js#L33
const { customPromisifyArgs } = require('internal/util');
// https://github.com/nodejs/node/blob/v12.x/lib/dns.js#L159
ObjectDefineProperty(lookup, customPromisifyArgs,
{ value: ['address', 'family'], enumerable: false });
复制代码
customPromisifyArgs
customPromisifyArgs 这个参数是从 internal/util 模块导出的,仅内部调用,所以咱们在外部 util.promisify 上是没有这个参数的。
也意味着只有 Nodejs 模块中例如 dns.klookup()、fs.read() 等方法在多参数的时候可使用 util.promisify 转为 Promise,若是咱们自定义的 callback 存在多参数的状况,使用 util.promisify 则不行,固然,若是你有须要也能够基于 util.promisify 本身封装一个。
// https://github.com/nodejs/node/blob/v12.x/lib/internal/util.js#L429
module.exports = {
...
// Symbol used to customize promisify conversion
customPromisifyArgs: kCustomPromisifyArgsSymbol,
};
复制代码
util.promisify 核心实现解析
// https://github.com/nodejs/node/blob/v12.x/lib/internal/util.js#L277
const kCustomPromisifyArgsSymbol = Symbol('customPromisifyArgs'); // {1}
function promisify(original) {
...
// 获取多个回调函数的参数名称列表
const argumentNames = original[kCustomPromisifyArgsSymbol]; // {2}
function fn(...args) {
return new Promise((resolve, reject) => {
try {
// (err, result) 改成 (err, ...values) {3}
original.call(this, ...args, (err, ...values) => {
if (err) {
reject(err);
} else {
// 变更之处 -> start
// argumentNames 存在且 values > 1,则回调会存在多个参数名称,进行遍历,返回一个 obj
if (argumentNames !== undefined && values.length > 1) { // {4}
const obj = {};
for (let i = 0; i < argumentNames.length; i++)
obj[argumentNames[i]] = values[i];
resolve(obj);
} else { // {5} 不然 values 最多仅有一个参数名称,即数组 values 有且仅有一个元素
resolve(values[0]);
}
// end <- 变更之处
}
});
} catch(err) {
reject(err);
}
});
}
return fn;
}
复制代码
上面第1、第二节咱们自定义实现的 mayJumPromisify 分别实现了含有 (err, result) => ... 和自定义 Promise 函数功能。
第三节中介绍的回调函数多参数转换,因为 kCustomPromisifyArgsSymbol 使用 Symbol 声明(每次从新定义都会不同),且没有对外提供,若是要实现第三个功能,须要咱们每次在 callback 函数上从新定义 kCustomPromisifyArgsSymbol 属性。
例如,如下定义了一个 callback 函数用来获取用户信息,返回值是多个参数 name、age,经过定义 kCustomPromisifyArgsSymbol 属性,便可使用咱们本身写的 mayJunPromisify 来转换为 Promise 形式。
function getUserById(id, cb) {
const name = '张三', age = 20;
cb(null, name, age);
}
Object.defineProperty(getUserById, kCustomPromisifyArgsSymbol, {
value: ['name', 'age'], enumerable: false
})
const getUserByIdPromisify = mayJunPromisify(getUserById);
getUserByIdPromisify(1)
.then(({ name, age }) => {
console.log(name, age);
})
.catch(err => console.log(err));
复制代码
自定义 mayJunPromisify 实现源码
https://github.com/Q-Angelo/project-training/tree/master/nodejs/module/promisify
复制代码
util.promisify 是 Nodejs 提供的一个实用工具函数用于将 callback 转为 promise,本节从基本使用 (err, result) => ... 转 Promise、自定义 Promise 函数重写 util.promisify 返回值、Promisify 回调函数的多参转换三个方面进行了讲解,在理解了其实现以后本身也能够实现一个相似的函数。