Node.js 源碼解析 util.promisify 如何將 Callback 轉爲 Promise

Nodejs util 模塊提供了不少工具函數。爲了解決回調地獄問題,Nodejs v8.0.0 提供了 promisify 方法能夠將 Callback 轉爲 Promise 對象。node

工做中對於一些老項目,有 callback 的一般也會使用 util.promisify 進行轉換,以前更可能是知其然不知其因此然,本文會從基本使用和對源碼的理解實現一個相似的函數功能。git

1. Promisify 簡單版本實現

在介紹 util.promisify 的基礎使用以後,實現一個自定義的 util.promisify 函數的簡單版本。github

1.1 util promisify 基本使用

將 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));
複製代碼

1.2 自定義 mayJunPromisify 函數實現

自定義 mayJunPromisify 函數實現 callback 轉換爲 promise,核心實現以下:

  • 行 {1} 校驗傳入的參數 original 是否爲 Function,不是則拋錯
  • promisify(fs.readFile) 執行以後會返回一個函數 fn,行 {2} 定義待返回的 fn 函數,行 {3} 處返回
  • fn 返回的是一個 Promise 對象,在返回的 Promise 對象裏執行 callback 函數
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));
複製代碼

2. Promisify 自定義 Promise 函數版本實現

另外一個功能是可使用 util.promisify.custom 符號重寫 util.promisify 返回值。

2.1 util.promisify.custom 基本使用

在 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)); // 該文件暫時禁止讀取
複製代碼

2.2 自定義 mayJunPromisify.custom 實現

  • 定義一個 Symbol 變量 kCustomPromisifiedSymbol 賦予 mayJunPromisify.custom
  • 行 {1} 校驗是否有自定義的 promise 化函數
  • 行 {2} 自定義的 mayJunPromisify.custom 也要保證是一個函數,不然拋錯
  • 行 {3} 直接返回自定義的 mayJunPromisify.custom 函數,後續的 fn 函數就不會執行了,所以在這塊也就重寫了 util.promisify 返回值
// 因此說 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)); // 該文件暫時禁止讀取
複製代碼

3. Promisify 回調函數的多參轉換

一般狀況下咱們是 (err, value) => ... 這種方式實現的,結果只有 value 一個參數,可是呢有些例外狀況,例如 dns.lookup 它的回調形式是 (err, address, family) => ... 擁有三個參數,一樣咱們也要對這種狀況作兼容。

3.1 util.promisify 中的基本使用

和上面區別的地方在於 .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));
複製代碼

3.2 util.promisify 實現解析

相似 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 核心實現解析

參見源碼 internal/util.js#L277

  • 行 {1} 定義 Symbol 變量 kCustomPromisifyArgsSymbol
  • 行 {2} 獲取參數名稱列表
  • 行 {3} (err, result) 改成 (err, ...values),原先 result 僅接收一個參數,改成 ...values 接收多個參數
  • 行 {4} argumentNames 存在且 values > 1,則回調會存在多個參數名稱,進行遍歷,返回一個 obj
  • 行 {5} 不然 values 最多僅有一個參數名稱,即數組 values 有且僅有一個元素
// 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;
}
複製代碼

4. 實現一個完整的 promisify

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

相關文章
相關標籤/搜索