callback函数_Nodejs 源码解析 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); // 转化为 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相关推荐
- 如何将文件地址转为url_Node.js 源码解析 util.promisify 如何将 Callback 转为 Promise
Nodejs util 模块提供了很多工具函数.为了解决回调地狱问题,Nodejs v8.0.0 提供了 promisify 方法可以将 Callback 转为 Promise 对象. 工作中对于一些 ...
- underscoreJs中pluck函数的源码解析
9月份之后项目开始进入收尾期了,产品要上市,所以9月之后的两个月都在疯狂的改BUG.最近总算是基本结束了,只剩下扫尾的了.终于能静下心来好好研究技术了.最近遇到两个函数,分别是underscore中的 ...
- bind函数polyfill源码解析
准备知识 使用new来调用函数会自动执行下面的操作: 创建一个全新的对象 这个新对象会被执行原型连接 这个新对象会绑定到函数调用的this 如果函数没有返回其他对象,那么new表达式中的函数调用会自动 ...
- java的resize函数_Java源码解析HashMap的resize函数
hashmap的resize函数,用于对hashmap初始化或者扩容. 首先看一下该函数的注释,如下图.从注释中可以看到,该函数的作用是初始化或者使table的size翻倍.如果table是null, ...
- QT源码解析(一) QT创建窗口程序、消息循环和WinMain函数
版权声明 请尊重原创作品.转载请保持文章完整性,并以超链接形式注明原始作者"tingsking18"和主站点地址,方便其他朋友提问和指正. QT源码解析(一) QT创建窗口程序.消 ...
- cvHoughLines2霍夫直线检测函数详解及源码解析
转载请注明出处. 文章链接:https://blog.csdn.net/duiwangxiaomi/article/details/126406184 博文目录 一. 前言 二. cvHoughLin ...
- Vue源码解析(一)
前言:接触vue已经有一段时间了,前面也写了几篇关于vue全家桶的内容,感兴趣的小伙伴可以去看看,刚接触的时候就想去膜拜一下源码~可每次鼓起勇气去看vue源码的时候,当看到几万行代码的时候就直接望而却 ...
- snabbdom源码解析(七) 事件处理
事件处理 我们在使用 vue 的时候,相信你一定也会对事件的处理比较感兴趣. 我们通过 @click 的时候,到底是发生了什么呢! 虽然我们用 @click绑定在模板上,不过事件严格绑定在 vnode ...
- Promise-Polyfill源码解析(2)
在上篇文章Promise-Polyfill源码解析(1)详细分析了Promise构造函数部分的源码,本篇我们继续分析剩下的源码. 本篇我们重点分析then方法,让我们回忆下then方法的使用方式:首先 ...
最新文章
- 现代化机器学习工具,助数据科学家开展更多工程或业务功能
- 也许你不知道的c#基本数据类型及其默认值
- 十大滤波算法程序大全
- ps cs3中显示任何像素不大于50%选择。选区边将不可见是什么意思
- 帆软决策报表嵌入html,在决策报表中使用网页框控件
- [POJ 1742] Coins 【DP】
- android懒加载单实例,【 Android 10 设计模式 】系列 -- 单例
- 鱼塘钓鱼(信息学奥赛一本通-T1373)
- python+selenium,实现带有验证码的自动化登录功能
- android httppost
- python中元组和列表的区别_Python 序列:列表、元组
- 苹果cms卫视直播html源码,苹果CMS如何使用默认模板新建一个直播页面?
- (2016.12.02更新)CnCrypt文件保险柜1.18,兼容TrueCrypt加密卷,单文件绿色版
- 正则匹配emjio表情
- [WARNING]: Could not match supplied host pattern, ignoring: servers
- 论文阅读笔记--Predicting Human Eye Fixations via an LSTM-based Saliency Attentive Model
- TCP IP地址和端口号设置
- 投资高手三十年投资经验总结的18条真谛
- 1.5.6.六种常见的三角关系
- 硬阈值(Hard Thresholding)函数解读
热门文章
- 如何组织软件模块的代码结构?
- ncurses其他特性:curs_set(),离开curses模式,ACS_扩展字符集,扩展库
- java 水表识别_水表识别 --数字的分割
- python修饰符的理解_python函数修饰符@的使用方法解析
- windows ce6.0系统 支持双网卡吗_MacBook双系统不求人,自己来
- 电脑显示器不亮主机正常_电脑显示屏不亮但是主机已开机怎么解决
- asp.net listview 字段太多 滚动条_高考英语阅读理解生僻单词太多怎么办?十大招数帮到你...
- Django:静态文件staticfiles
- Spring Data说明
- php curl 发送checkbox,使用curl 提交表单(多维数组+文件)数据到服务器的有关问题...