點燈坊

失くすものさえない今が強くなるチャンスよ

使用 promisify() 將 Callback-based Function 轉成 Promise-based Function

Sam Xiao's Avatar 2019-09-26

在 ECMAScript 5 以 Callback 實現 Asynchronous,如 Node 所提供的 Function 就是 Callback-Based,我們可實作一個 Higher Order Function 專門將 Callback-Based Function 轉成 ECMAScript 2015 的 Promise-Based Function。

Version

macOS Mojave 10.14.6
VS Code 1.38.1
Node 12.10.0
ECMAScript 2015
Wink-fp 0.0.20

Callback-based Function

let fs = require('fs');

fs.readFile(__filename, 'utf8', (err, data) => {
  if (err) console.log(err);
  else console.log(data);
});

fs.readFile() 為 Node 代表性 function,負責讀取文字檔內容:

  • 第一個 argument 為 檔案名稱
  • 第二個 argument 為 encoding
  • 第三個 argument 為 callback,負責讀取文字內容與 error handling

Node 的 callback 習慣第一個 argument 為 error,第二個 argument 為 data,使用對 errif else 判斷處理 data

Promise-based Function

let fs = require('fs');

let readFile = (path, encoding) => new Promise((resolve, reject) => {
  fs.readFile(path, encoding, (err, data) => {
    if (err) reject(err);
    else resolve(data);
  });
});

readFile(__filename, 'utf8')
  .then(x => console.log(x))
  .catch(e => console.error(e));

我們可自行實作 readFile() 回傳 promise,則取 datathen() 的 callback,處理 errcatch() 的 callback,這種 promise-based function 明顯比 callback-based function 可讀性高。

第 3 行

let readFile = (path, encoding) => new Promise((resolve, reject) => {
  fs.readFile(path, encoding, (err, data) => {
    if (err) reject(err);
    else resolve(data);
  });
});

其實readFile() 底層仍然是呼叫 fs.readFile(),只是使用了 promise constructor 做包裝,若 if (err) 成立則呼叫 reject() 產生 rejected promise,否則呼叫 resolve() 產生 fulfilled promise。

Higher Order Function

let fs = require('fs');

let promisify = fn => (...args) => new Promise((resolve, reject) => 
  fn(...args, (err, data) => (err) ? reject(err): resolve(data)));

let readFile = promisify(fs.readFile);

readFile(__filename, 'utf8')
  .then(x => console.log(x))
  .catch(e => console.error(e));

觀察 readFile(),其實使用 promise constructor 有一定的套路,因此可自行實作 promisify() higher order function,專門將 callback-based function 轉成 promise-based function。

第 3 行

let promisify = fn => (...args) => new Promise((resolve, reject) => 
  fn(...args, (err, data) => (err) ? reject(err): resolve(data)));

由於 promisify() 接受任意 argument 的 function,因此使用了 ES6 的 ...arg rest argument。

Wink-fp

let fs = require('fs');
let util = require('util');

let readFile = util.promisify(fs.readFile);

readFile(__filename, 'utf8')
  .then(x => console.log(x))
  .catch(e => console.error(e));

Node 已經提供了 util.promisify(),可直接使用。

前端則可使用 Wink-fp 的 promisify() 將 callback-based function 轉成 promise-based function。

promisify()
(a...,(a... -> b)) -> (a... -> Promise)
將 callback-based function 轉成 promised-based function

(a...,(a... -> b)):callback-based function
(a... -> Promise):回傳為 promise-based function

Conclusion

  • ES6 提出 promise 後,理論上都可用 promise 取代 callback
  • 透過 Node 的 util.promisify() 或 Wink-fp 的 promisify(),可將 callback-based function 轉成 promise-based function
  • Node 的 callback-based function 有其一定的 signature,但前端 function 多半 signature 不固定,但抽 higher order function 的套路是一樣的

Reference

Marius Schulz, Convert a Callback-Based JavaScript Function to a Promise-Based One