每天阅读一个 npm 模块(6)- pify
系列文章:
- 每天阅读一个 npm 模块(1)- username
- 每天阅读一个 npm 模块(2)- mem
- 每天阅读一个 npm 模块(3)- mimic-fn
- 每天阅读一个 npm 模块(4)- throttle-debounce
- 每天阅读一个 npm 模块(5)- ee-first
之前阅读的 npm 模块都来源于 awesome-micro-npm-packages 这个项目,不过浏览了一些之后,发现好多都不太适合拿来做源码学习。如果读者有推荐的适合的模块,欢迎在评论区指出 😊
一句话介绍
今天阅读的模块是 pify,通过它可以将很多采用 callback 方式进行调用的函数变成 Promise 调用,甚至采用 async/await 语法进行异步调用,从而可以在不修改调用函数的情况下避免回调地狱,也可以让代码具有更好的可读性,当前的版本是 4.0.0,周下载量约为 750 万。
用法
以 Node.js 中异步读取文件为例,常用的方法之一就是 fs.readFile(path, encoding, callback)
,这种通过回调函数进行异步操作的方式在以前的代码中十分常见 ,也是迫不得已。但是当如今拥有了 Promise 之后,这样写就显得十分麻烦,也不易于维护,所以可以通过 pify
这个模块将他们 Promise 化(即 Promisify)。
const fs = require('fs');
const pify = require('pify');
// 将 fs.readFile 变成 Promise 调用
pify(fs.readFile)('package.json', 'utf8').then(data => {
console.log(JSON.parse(data).name);
// => 'pify'
});
// 通过 Promise 化函数,使用 async/await 语法
(async function(){
const data = await pify(fs.readFile)('package.json', 'utf-8');
console.log(JSON.parse(data));
// => 'pify'
})();
除了直接对一个函数进行 Promise 化外,还可以对一整个模块中的每一个函数进行 Promise 化:
const fs = require('fs');
const pify = require('pify');
// 将 fs 模块 Promise 化
pify(fs).readFile('package.json', 'utf8').then(data => {
console.log(JSON.parse(data).name);
// => 'pify'
});
源码学习
函数 Promise 化
// 源码 6-1
module.exports = (input) => {
let ret;
if (typeof input === 'function') {
ret = (...args) => processFn(input)(...args);
}
return ret;
}
pify
主函数入口十分简单,如果传入的参数为函数,则经过 processFn
处理后作为结果返回,这里两个 ...args
虽然看起来一样,但实际上是 ES6新增的不同语法:
-
第一个
...args
用法叫做函数 rest 参数,可以用来获取函数的多余参数。它不同于arguments
是一个类数组的类型,而是一个数组的实例:function foo(name, ...rest) { console.log(rest, rest instanceof Array); } foo('Elvin', 'likes', 'JavaScript'); // => [ 'likes', 'JavaScript' ], true
-
第二个
...args
的用法叫做扩展运算符(spread),类似于 rest 参数的逆运算,将一个数组进行展开:const x = [1, 2, 3]; const y = [...x, 4]; console.log(...x); // => 1 2 3 console.log(y); // =>[ 1, 2, 3, 4 ]
这里实际上没有必要进行一层包裹,可以直接返回
processFn
处理的函数,即变成ret = processFn(input)
,我也根据这个想法提出了 pify - PR#65。
接下来看一看 processFn
这个函数的具体实现。这个函数也十分简单,主要做了四件事情:
- 构造一个 Promise 并将其作为函数的返回值。
- 构造一个 callback 函数,在这个函数中,假如有错误,则调用
Promise.reject()
方法抛出异常;假如无错误,则调用Promise.resolve()
返回正常结果。 - 对于传入的参数
args
通过 push 方法追加我们刚刚构造的 callback 函数,从而形成完整的参数。 - 最后通过
fn.apply(this, args)
调用原函数。
// 源码 6-2
const processFn = (fn) => function (...args) {
return new Promise((resolve, reject) => {
args.push((error, result) => {
if (error) {
reject(error);
} else {
resolve(result);
}
});
fn.apply(this, args);
});
};
对象 Promise 化
对象的 Promise 化其实就是遍历对象的每一个属性,如果属性类型为函数的话,那么就用上节所说的 processFn
进行处理;如果属性类型不为函数的话,则直接返回:
// 源码 6-3
module.exports = (input) => {
for (const key in input) {
const property = input[key];
ret[key] = typeof property === 'function' ? processFn(property) : property;
}
}
写在最后
今天阅读的 pify 模块的代码其实不难,但是它的的确确解决了开发过程中的痛点,所以它能在 Github -pify 上获得 1000+ 的赞,在 npm 上每周的下载量高达 750 万。
另外从 Node.js 8.0 起,就内置了 util.promisify(fn)
方法,可以实现部分 pify 的功能,官方文档可以参考 Node.js - util.promisify,关于两者的区别可以参考 How does this differ from util.promisfy,主要为两点:
pify
支持 Node.js 6.0 及以上版本,util.promisify(fn)
只支持 Node.js 8.0 及以上版本。pify
支持对整个模块 Promise 化,util.promisify(fn)
只支持对单个函数的 Promise 化。
关于我:毕业于华科,工作在腾讯,elvin 的博客 欢迎来访 ^_^
每日系列很棒,来赞一个
学习了,感谢分享
赞
@aojiaotage 谢谢,欢迎持续关注~
@DCbryant 谢谢😊
赞
来自酷炫的 CNodeMD
方向对的,多看优秀代码,才能写出优秀代码
来自拉风的 Taro-cnode
这个代码,帮助对promise的灵活理解和应用了。
我竟然看懂了