绑定完请刷新页面
取消
刷新

分享好友

×
取消 复制
Node.js 源码解析 util.promisify 如何将 Callback 转为 Promise
2020-04-14 10:00:52

Nodejs util 模块提供了很多工具函数。为了解决回调地狱问题,Nodejs v8.0.0 提供了 promisify 方法可以将 Callback 转为 Promise 对象。

工作中对于一些老项目,有 callback 的通常也会使用 util.promisify 进行转换,之前更多是知其然不知其所以然,本文会从基本使用和对源码的理解实现一个类似的函数功能。

1. Promisify 简单版本实现

在介绍 util.promisify 的基础使用之后,实现一个自定义的 util.promisify 函数的简单版本。

1.1 util promisify 基本使用

将 callback 转为 promise 对象,首先要确保这个 callback 为一个错误优先的回调函数,即 (err, value) => ... err 指定一个错误参数,value 为返回值。

以下将以 fs.readFile 为例进行介绍。

创建一个 text.txt 文件

创建一个 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 获取相应结果

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

上面、第二节我们自定义实现的 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 回调函数的多参转换三个方面进行了讲解,在理解了其实现之后自己也可以实现一个类似的函数。

分享好友

分享这个小栈给你的朋友们,一起进步吧。

Nodejs技术栈
创建时间:2020-06-09 14:34:40
专注 Nodejs 相关技术栈分享
展开
订阅须知

• 所有用户可根据关注领域订阅专区或所有专区

• 付费订阅:虚拟交易,一经交易不退款;若特殊情况,可3日内客服咨询

• 专区发布评论属默认订阅所评论专区(除付费小栈外)

栈主、嘉宾

查看更多
  • highhand
    栈主

小栈成员

查看更多
  • cctx2021
戳我,来吐槽~