专栏首页Nodejs技术栈Node.js 源码解析 util.promisify 如何将 Callback 转为 Promise

Node.js 源码解析 util.promisify 如何将 Callback 转为 Promise

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技术栈(NodejsRoadmap),作者:五月君

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-04-12

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Node.js内存管理和V8垃圾回收机制

    对于 Node.js 服务端研发的同学来说,关于垃圾回收、内存释放这块不需要向 C/C++ 的同学那样在创建一个对象之后还需要手动创建一个 delete/fre...

    五月君
  • Node.js v13.3.0(Current)发布

    Node.js v13.3.0(Current)于 2019-12-03 发布,以下为一些显著的的变化。Node.js 版本不清楚的可以参考 Node.js 版...

    五月君
  • 实现一个二叉搜索树(JavaScript 版)

    二叉树在计算机科学中应用很广泛,学习它有助于让我们写出高效的插入、删除、搜索节点算法。二叉树的节点定义:一个节点最多只有两个节点,分别为左侧节点、右侧节点。

    五月君
  • Linux内核中container_of函数详解

    在Linux 内核中,container_of 函数使用非常广,例如 Linux内核链表 list_head、工作队列work_struct中 在Linux 内...

    小小科
  • 大数据服务上云的思考

    说说大数据怎么上云的一些思考: 1、首先说说,大数据和云的关系。云是一种网络形态的概念,是继1980年代大型计算机到客户端-服务器的大转变之后的又一种巨变。云计...

    大数据和云计算技术
  • golang 的container/list (一)

    问题1:有了slice,还要list做什么? 问题2:list的底层实现是什么? 带着这两个问题来什么有浅入深的学习golang 语言 。 首先来看...

    地球流浪猫
  • 5G技术的相关知识

    第五代移动电话行动通信标准,也称第五代移动通信技术,外语缩写:5G。也是4G之后的延伸,正在研究中,5G网络的理论下行速度为10Gb/s(相当于下载速度1.25...

    用户5777378
  • Facebook数据泄漏受害者: 既然无法阻止个人数据被卖, 不如我自己先卖

    众所周知,我们在社交网站上和智能设备上产生的数据正变得越来越有价值,很多巨头都在通过将这些数据出售来获利。尽管大部分用户都对此颇有微词,但似乎这一趋势已无法阻挡...

    区块链大本营
  • 泛微e-cology OA Beanshell组件远程代码执行漏洞复现

    泛微e-cologyOA协同商务系统是专为大中型企业制作的OA办公系统,支持PC端、移动端和微信端同时办公,完美的解决了距离的问题,内置大量智能化办公工具,让各...

    墙角睡大觉
  • 43.QT-访问远程SQLite数据库

    将要共享的share文件夹设置为共享(远程访问默认只能读),如果要想远程访问能够写的话,则点击权限进行修改,然后应用.

    张诺谦

扫码关注云+社区

领取腾讯云代金券