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); // 转化为 promisereadFilePromisify('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')}// 变动之处 -> startif (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 conversioncustomPromisifyArgs: 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,则回调会存在多个参数名称,进行遍历,返回一个 objif (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 回调函数的多参转换三个方面进行了讲解,在理解了其实现之后自己也可以实现一个类似的函数。

callback函数_Nodejs 源码解析 util.promisify 如何将 Callback 转为 Promise相关推荐

  1. 如何将文件地址转为url_Node.js 源码解析 util.promisify 如何将 Callback 转为 Promise

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

  2. underscoreJs中pluck函数的源码解析

    9月份之后项目开始进入收尾期了,产品要上市,所以9月之后的两个月都在疯狂的改BUG.最近总算是基本结束了,只剩下扫尾的了.终于能静下心来好好研究技术了.最近遇到两个函数,分别是underscore中的 ...

  3. bind函数polyfill源码解析

    准备知识 使用new来调用函数会自动执行下面的操作: 创建一个全新的对象 这个新对象会被执行原型连接 这个新对象会绑定到函数调用的this 如果函数没有返回其他对象,那么new表达式中的函数调用会自动 ...

  4. java的resize函数_Java源码解析HashMap的resize函数

    hashmap的resize函数,用于对hashmap初始化或者扩容. 首先看一下该函数的注释,如下图.从注释中可以看到,该函数的作用是初始化或者使table的size翻倍.如果table是null, ...

  5. QT源码解析(一) QT创建窗口程序、消息循环和WinMain函数

    版权声明 请尊重原创作品.转载请保持文章完整性,并以超链接形式注明原始作者"tingsking18"和主站点地址,方便其他朋友提问和指正. QT源码解析(一) QT创建窗口程序.消 ...

  6. cvHoughLines2霍夫直线检测函数详解及源码解析

    转载请注明出处. 文章链接:https://blog.csdn.net/duiwangxiaomi/article/details/126406184 博文目录 一. 前言 二. cvHoughLin ...

  7. Vue源码解析(一)

    前言:接触vue已经有一段时间了,前面也写了几篇关于vue全家桶的内容,感兴趣的小伙伴可以去看看,刚接触的时候就想去膜拜一下源码~可每次鼓起勇气去看vue源码的时候,当看到几万行代码的时候就直接望而却 ...

  8. snabbdom源码解析(七) 事件处理

    事件处理 我们在使用 vue 的时候,相信你一定也会对事件的处理比较感兴趣. 我们通过 @click 的时候,到底是发生了什么呢! 虽然我们用 @click绑定在模板上,不过事件严格绑定在 vnode ...

  9. Promise-Polyfill源码解析(2)

    在上篇文章Promise-Polyfill源码解析(1)详细分析了Promise构造函数部分的源码,本篇我们继续分析剩下的源码. 本篇我们重点分析then方法,让我们回忆下then方法的使用方式:首先 ...

最新文章

  1. 现代化机器学习工具,助数据科学家开展更多工程或业务功能
  2. 也许你不知道的c#基本数据类型及其默认值
  3. 十大滤波算法程序大全
  4. ps cs3中显示任何像素不大于50%选择。选区边将不可见是什么意思
  5. 帆软决策报表嵌入html,在决策报表中使用网页框控件
  6. [POJ 1742] Coins 【DP】
  7. android懒加载单实例,【 Android 10 设计模式 】系列 -- 单例
  8. 鱼塘钓鱼(信息学奥赛一本通-T1373)
  9. python+selenium,实现带有验证码的自动化登录功能
  10. android httppost
  11. python中元组和列表的区别_Python 序列:列表、元组
  12. 苹果cms卫视直播html源码,苹果CMS如何使用默认模板新建一个直播页面?
  13. (2016.12.02更新)CnCrypt文件保险柜1.18,兼容TrueCrypt加密卷,单文件绿色版
  14. 正则匹配emjio表情
  15. [WARNING]: Could not match supplied host pattern, ignoring: servers
  16. 论文阅读笔记--Predicting Human Eye Fixations via an LSTM-based Saliency Attentive Model
  17. TCP IP地址和端口号设置
  18. 投资高手三十年投资经验总结的18条真谛
  19. 1.5.6.六种常见的三角关系
  20. 硬阈值(Hard Thresholding)函数解读

热门文章

  1. 如何组织软件模块的代码结构?
  2. ncurses其他特性:curs_set(),离开curses模式,ACS_扩展字符集,扩展库
  3. java 水表识别_水表识别 --数字的分割
  4. python修饰符的理解_python函数修饰符@的使用方法解析
  5. windows ce6.0系统 支持双网卡吗_MacBook双系统不求人,自己来
  6. 电脑显示器不亮主机正常_电脑显示屏不亮但是主机已开机怎么解决
  7. asp.net listview 字段太多 滚动条_高考英语阅读理解生僻单词太多怎么办?十大招数帮到你...
  8. Django:静态文件staticfiles
  9. Spring Data说明
  10. php curl 发送checkbox,使用curl 提交表单(多维数组+文件)数据到服务器的有关问题...