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 回調函數的多參轉換三個方面進行了講解,在理解了其實現以後本身也能夠實現一個相似的函數。