每天阅读一个 npm 模块(6)- pify
发布于 10 个月前 作者 elvinn 1791 次浏览 来自 分享

每天阅读一个 npm 模块(6)- pify

系列文章:

  1. 每天阅读一个 npm 模块(1)- username
  2. 每天阅读一个 npm 模块(2)- mem
  3. 每天阅读一个 npm 模块(3)- mimic-fn
  4. 每天阅读一个 npm 模块(4)- throttle-debounce
  5. 每天阅读一个 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 这个函数的具体实现。这个函数也十分简单,主要做了四件事情:

  1. 构造一个 Promise 并将其作为函数的返回值。
  2. 构造一个 callback 函数,在这个函数中,假如有错误,则调用 Promise.reject() 方法抛出异常;假如无错误,则调用 Promise.resolve() 返回正常结果。
  3. 对于传入的参数 args 通过 push 方法追加我们刚刚构造的 callback 函数,从而形成完整的参数。
  4. 最后通过 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,主要为两点:

  1. pify 支持 Node.js 6.0 及以上版本, util.promisify(fn) 只支持 Node.js 8.0 及以上版本。
  2. pify 支持对整个模块 Promise 化, util.promisify(fn) 只支持对单个函数的 Promise 化。

关于我:毕业于华科,工作在腾讯,elvin 的博客 欢迎来访 ^_^

10 回复

每日系列很棒,来赞一个

学习了,感谢分享

@aojiaotage 谢谢,欢迎持续关注~

方向对的,多看优秀代码,才能写出优秀代码

来自拉风的 Taro-cnode

这个代码,帮助对promise的灵活理解和应用了。

我竟然看懂了

回到顶部