当前位置: 首页 > 后端技术 > Node.js

util.promisify那些事

时间:2023-04-03 18:19:53 Node.js

util.promisify是node.js8.x版本的一个新工具,用于将老式的Errorfirstcallback转为Promise对象,让旧项目的改造更简单easy.在这个工具正式上线之前,民间已经有很多类似的工具,比如es6-promisify、thenify、bluebird.promisify。而很多其他优秀的工具已经实现了这样的功能,让我们在处理老项目的时候不用再费心用Promise重新实现各种代码了。工具实现的总体思路首先需要说明一下这个工具的总体思路,因为Node中对于异步回调有一个约定:Errorfirst,也就是说回调函数中的第一个参数必须是一个错误对象,其余参数在正确时是数据。了解这些规则后,该工具很容易实施。当第一个参数匹配到某个值时,会触发reject,其他情况会触发resolve。一个简单的示例代码:functionutil(func){return(...arg)=>newPromise((resolve,reject)=>{func(...arg,(err,arg)=>{if(err)reject(err)elseresolve(arg)})})}调用效用函数返回一个匿名函数,匿名函数接收原函数的参数。调用匿名函数后,根据这些参数调用真正的函数,同时拼接一个处理结果的回调。检测到err有值,触发reject,其他情况触发resolveresolve。只能传入一个参数,所以不需要在callback中使用...arg来获取所有的返回值。常规用法以官方文档中的const{promisify}=require('util')constfs=require('fs')conststatAsync=promisify(fs.stat)statAsync('.').then(stats=>{//得到了正确的数据},err=>{//发生了异常})因为它是一个Promise,我们可以使用await来进一步简化代码:const{promisify}=require('util')constfs=require('fs')conststatAsync=promisify(fs.stat)//假设在异步函数中try{conststats=awaitstatAsync('.')//得到正确的结果}catch(e){//exceptionoccurs}用法与其他工具没有太大区别,我们可以很容易地将回调转换为Promises并将其应用到新项目中。自定义Promiseization中有一些场景是不能直接使用promisify转换的。大致有两种情况:不遵循Errorfirst回调约定的回调函数返回多个参数,返回多个参数的回调函数是第一个。如果不按照我们的约定,可能会导致误判拒收,得不到正确的反馈。至于第二项,是因为Promise.resolve只能接受一个参数,多余的参数会被忽略。所以为了得到正确的结果,我们可能需要手动实现相应的Promise函数,但是自己实现之后,并不能保证用户不会为你的函数调用promisify。因此util.promisify也提供了一个Symbol类型的key,util.promisify.custom。了解Symbol类型的人应该都知道,它是一个唯一值。这是用于指定自定义Promise结果的util.prosimify。用法如下:const{promisify}=require('util')//例如我们有一个对象,它提供了一个返回多个参数的回调版本的函数constobj={getData(callback){callback(null,'Niko',18)//返回两个参数,名字和年龄}}//这个时候肯定不能使用promisify//因为Promise.resolve只接受一个参数,所以我们只会得到Nikopromisify(obj.getData)().then(console.log)//Niko//所以我们需要使用promisify。custom来自自定义处理方法obj.getData[promisify.custom]=async()=>({name:'Niko',age:18})//当然这是曲线救国的方法,反正Promise不会返回多个参数的Promisify(obj.getData)().then(console.log)//{name:'Niko',age:18}我有一个大胆的想法,为什么Promise不能解析多个值。一个没有考证的原因,强行解释:如果可以解析多个值,如何让async函数返回(这句话看的开心,不要当真)不过应该和return有关,因为Promise是可以链式调用的,所以在每个Promise中执行完then之后,它的返回值都会作为一个新的Promise对象resolve的值。在JavaScript中是没有办法返回多个参数的,所以即使是第一个Promise也可以返回多个参数,只要return的处理就会丢失。在使用中,在可能调用promisify的函数中添加promisify.custom的相应处理就很简单了。后续代码调用promisify时会判断:如果目标函数有promisify.custom属性,会判断其类型:如果不是可执行函数,则抛出异常如果是可执行函数,则直接返回其对应的如果目标函数没有对应的属性,则根据Error优先回调约定生成对应的处理函数然后返回。添加此自定义属性后,您不必担心为您的函数调用promisify。并且可以验证赋值给custom的函数地址和promisify返回的函数是一样的:obj.getData[promisify.custom]=async()=>({name:'Niko',age:18})//上面对async函数的赋值也可以改成普通函数,只要普通函数返回一个Promise实例即可//这两个方法完全等同于上面的asyncobj.getData[promisify.custom]=()=>Promise.resolve({name:'Niko',age:18})obj.getData[promisify.custom]=()=>newPromise(resolve({name:'Niko',age:18}))console.log(obj.getData[promisify.custom]===promisify(obj.getData))//true一些内置的自定义处理在一些内置的包中,也能找到promisify.custom的痕迹,比如最常用的child_process.execpromisify.custom的处理是内置的:const{exec}=require('child_process')const{promisify}=require('util')console.log(typeofexec[promisify.custom])//function因为就像前面例子中提到的曲线救国的官方方法,就是以函数签名中的参数名作为key,将其所有的参数存储在一个Object对象中进行返回。例如child_process.exec的返回值会包含两个,stdout和stderr,一个是命令执行后的正确输出,一个是命令执行后的错误输出:promisify(exec)('ls').then(console.log)//->{stdout:'XXX',stderr:''}或者我们故意输入一些错误的命令,当然这个只能在catch模块下捕获,而stderr的正常命令执行会为空String:promisify(exec)('lss').then(console.log,console.error)//->{...,stdout:'',stderr:'lss:commandnotfound'}包括setTimeout之类的东西,setImmediate也实现了相应的promisify.custom。为了实现sleep操作,手动用Promise封装了setTimeout:constsleep=promisify(setTimeout)console.log(newDate())awaitsleep(1000)console.log(newDate())内置promisify转换function如果你的Node版本使用10.x以上,你也可以从很多内置模块中找到类似.promises的子模块,里面包含了本模块常用的回调函数的Promise版本(都是async函数),不需要手动执行promisify转换。而且我个人认为这是一个很好的引导,因为之前的工具实现有的选择直接覆盖原来的函数,有的在原来的函数名后面加上Async来区分。官方的单独在模块中。引入一个子模块,并在其中实现Promise版本的功能。其实这样用起来很方便。以fs模块为例://之前介绍一些fs相关的API是这样的。const{readFile,stat}=require('fs')//现在可以轻松更改为const{readFile,stat}=require('fs').promises//或const{promises:{readFile,stat}}=require('fs')接下来要做的是删除与调用promisify相关的代码。对于使用该API的其他代码,这种变化是难以察觉的。所以如果你的node版本足够高,可以在使用内置模块前先阅读文档看看有没有对应的promises支持。如果有实现,可以直接使用。promisify的一些注意事项必须遵守错误优先回调协议,不能返回多个参数。注意要转换的函数是否包含对this的引用。前两个问题可以通过使用上面提到的promisify.custom来解决。但是第三项在某些情况下可能会被我们忽略。这不是promisify独有的问题。只是一个非常简单的例子:constobj={name:'Niko',getName(){returnthis.name}}obj.getName()//Nikoconstfunc=obj.getNamefunc()//undefined同样,如果我们做一些事情像这样在转换Promise的时候,可能会导致生成函数的this点出现问题。有两种方法可以解决这样的问题:也推荐使用箭头函数。在调用promisify之前使用bind绑定对应的this。但是这个问题也是基于promisify转换的函数赋值给其他变量的。.如果是这样的代码,那么就不用担心这个指向的问题了:constobj={name:'Niko',getName(callback){callback(null,this.name)}}//这样的操作不用担心this指向问题的obj.XXX=promisify(obj.getName)//如果赋值给其他变量,那么这里需要注意this指向constfunc=promisify(obj.getName)//这个错误的总结我认为Promise作为现代JavaScript异步编程的核心部分,了解如何将旧代码转换为Promise是一件非常有趣的事情。而我去了解官方工具是因为在搜索Redis相关的Promise版本时看到了这个readme:Thispackageislongermaintained。node_redis现在包括对核心承诺的支持,因此不再需要。然后跳转到node_redis中的实现方案中提到了util.promisify,于是抓起来研究了一下。感觉挺有意思的。我总结了一下,分享给大家。参考util.promisifychild_process.execfs.promises