每天閱讀一個 npm 模組(6)- pify

elvinnn發表於2018-09-04

系列文章:

  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 的部落格 歡迎來訪 ^_^

相關文章