Promisify 與 Callbackify — 你或許用不到,但了解一下也無妨

JavaScript 中有兩種風格迥異的呼叫非同步函數方法,分別是 Callback 與 Promise ,由於時空背景所限,早期的 API 設計往往是前者的回呼風格,這點在 Node 中尤其明顯,在時代交迭的溝渠中…

realdennis
5 min readMay 15, 2019

直接開始

由於單線程的原因 JavaScript 中有許多的非同步方法,而我們需要一個 callback 來讓非同步方法結束後呼叫。

dosomethingAsync(callback); // 非同步出去 完成後呼叫callback

不過由於將 callback 丟給這個函數,你往往不知道這個三方函式庫會怎麼執行它,有點控制權移轉的感覺…

而在 Node 中這類的回呼函數設計有一個大家認可的原則,名為「Error first callback」,其意思是回呼函數中第一個參數為錯誤實體。

你肯定常見到這種程式碼:

function callback (err,result){
if(err) {
console.log(err);
return;
}
// 操作result之類的
console.log(result);
}
doSomethingAsync(callback)

最常見的像是讀個檔案:

fs.readFile('/etc/passwd', (err, data) => {
if (err) throw err;
console.log(data);
});

都 9102 年了誰跟你在寫這種會 callback hell 的東西?

你可能會這樣想沒錯,不過你卻無法避開它,畢竟它曾是這個語言的歷史軌跡,也曾經是一部分的人所習慣的編寫風格。

Promisify

我們的主角 Promisify ,在 Node 的第 8 版以後,可以在 util 這個原生函式庫中找到它。

根據歷史推演我們可以發現 Promise 漸漸的普及在 JavaScript 社群,並且有更好的語義描述非同步程式的行為,比起回呼地獄,開發者更喜歡 then 與 catch 的鏈式呼叫(或是 await ),大概啦。

那麼這個函數在幹啥呢?其實很簡單,它幫你把原本吃 callback 的函數變成了 Promise 風格的函數。

它的用法是這樣:

const fs = require('fs')
const promisify = require('util').promisify;
const readFileAsync = promisify(fs.readFile);
readFileAsync('/etc/passwd').then( data =>{
// do something with data
}).catch( error =>{
// handle the error
})

我第一次見到這函數的時候,心情只能用一張圖來回答:

不過其實仔細想想會發現,promisify 僅能夠針對 Error-first callback 風格的函數做 Wrapper ,我們以 setTimeout 為例:

// setTimeout不是一個 error first 風格的函數
// 而且 callback 並非最後一個參數
setTimeout( callback , time )

很顯然不太一樣,就是反過來的概念,所以你要用這個 promisify 來把它給 wrapper 起來,只能這麼做:

const sleep = promisify( (ms,cb) => setTimeout(cb,ms) );
sleep(3000).then(()=>console.log('3 sec pass'));

真心不騙,promisify並不是超級人工智慧,當然不可能自動判讀每個 callback 長怎樣。

額外延伸:difference between ‘util.promisify(setTimeout)’ and ‘ms => new Promise(resolve => setTimeout(resolve, ms))’

Callbackify

有了Promisify 當然不能忘記介紹 callbackify 了,很明顯它是在做 promisify 類似的事,不過是反過來的,把 Promise 風格的函數轉變為 Error first callback。

const util = require('util');

async function fn() {
return 'hello world';
}
const callbackFunction = util.callbackify(fn);

callbackFunction((err, ret) => {
if (err) throw err;
console.log(ret);
});

這裡就不多加贅述了,我們直接進結論來探討吧。

結論

我們想從這篇文章探討一些文內所提到或是觀文後的疑惑:

  1. 為什麼標題說或許用不到?
  2. 為什麼 callbackify 會存在呢?

為什麼說或許用不到?

許多的函式庫現在都採用原生 Promise 支援, export 出來的的 API 已經不採用傳入 callback 的機制,這樣的設計方式也大幅減少文檔描述(特別撰寫開發者該如何設計 callback 的參數樣子等等的)。

例:如 request 有 request-promise、axios 則直接 promise based。

這點在我們剛剛舉的 filesystem 操作中就可以知道了,實際上 fs 早已經有原生 Promise 風格的版本了(不過上頭是寫實驗性質就是了)。

為什麼會有 callbackify ?

其實就是將兩種不同的程式寫作風格做變換,讓 Promise 風格的函式庫也能在 callback 風格的程式碼中被使用,有興趣的話可以看看當時提出的 issue

--

--