0%

Promise(XV) - Promisification - Promise 化

本文開始

Promisification(Promise 化) 是代表將一個 function 轉換成可以接受 callback function 並且這個 callback function 是會回傳 Promise 的。
會需要這樣將原本是 callback 機制做回傳的 function 或外部函式庫轉換成回傳 Promise 物件的機制是因為 Promise 操作起來比 callback function 更方便。
這一系列的教學文章中的提到過的 loadScript 為例

1
2
3
4
5
6
7
8
9
function loadScript(src, callback) {
const script = document.createElement('script');
script.src = src;

script.onload = callback(null, script);
script.onerror = callback(new Error(`Script load error for ${src}`));

document.head.append(script);
}

上面這個範例撰寫的形式就是都用 callback function 來做處理。
以下就把上面的範例 Promise 化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function loadScript(src, callback) {
let script = document.createElement('script');
script.src = src;

script.onload = () => callback(null, script);
script.onerror = () => callback(new Error(`Script load error for ${src}`), null);

document.head.append(script);
}

let loadScriptPromise = function(src) {
return new Promise((resolve, reject) => {
loadScript(src, (err, script) => {
if (err) reject(err);
else resolve(script);
});
});
};

loadScriptPromise('https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/js/bootstrap.min.js')
.then(res => console.log(res))
.catch(error => console.log(error))

可以看到上面範例的改動部分,就是把原本的傳入 loadScript 的 callback function 改成回傳 promise 的 callback function,也就是以下這一段程式碼

1
2
3
4
(err, script) => {
if(err) reject(err);
else resolve(script);
}

最後如果上面範例載入成功,應就可以在 console.log 裡面看到以下的內容

如果,把上面的網址改成
htxxxwefetps://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/js/bootstrap.min.js

這時候看 console.log 裡面的結果如下

Change promise-base into a helper

那如果,在專案中有多處都會需要有這種將原本 callback-base 的function 轉成 promise-base 的功能的化,那我們應該要把這種轉化的功能寫成一個公用的 helper。

首先先來看看, callback-base 的範例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const getSumAsync = (num1, num2, callback) => {
if(!num1 || !num2) return callback(new Error('Missing arguments'), null) ;

return callback(null, num1+ num2);
}

getSumAsync(1, 2, (err, result) => {
if(err) {
alert(err);
}
else {
console.log(result); // 3
}
})

Wrap into a promise

把上面的 callback-base function 改成用 Promise 的 resolve 和 reject 來回傳錯誤或執行成功的結果

1
2
3
4
5
6
7
8
9
10
const getSumAsync = (num1, num2) => {
return new Promise((resolve, reject) => {
if(!num1 || !num2) reject(new Error(`Missing arguments`));
resolve(num1 + num2);
})
}

getSumAsync(1, null)
.then(res => console.log(res))
.catch(alert)

Promisify

接下來,會跟著另一篇文章拆解原本這一篇教學裡面寫的 promisify function 的流程,一步一步操作。

NodeJS’s promisify

先來看看 Node.js 的 util.promisify() 的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const util = require('util');

const getSumAsync = (num1, num2, callback) => {
if(!num1 || !num2) return callback(new Error('Missing arguments'), null) ;

return callback(null, num1+ num2);
}

const getSumPromise = util.promisify(getSumAsync); // setp1
getSumPromise(1, 1) // setp2
.then(result => {
console.log(result);
})
.catch(err => {
// error hadnling
})

自訂義 promisify function

接下來,我們就來藉由解析 NodeJS 的 promisify 的方法,進而來撰寫自己的 promisify function

看看 step 1.

NodeJs 的 promisify function 會先丟進 function,由此先來定義一個可以丟入 function 的參數的 function

1
2
3
const myPromisify = fn => {};

const getSumPromise = myPromisify(getSumAsync);

看看 step 2.

getSumPromise(1, 1) 是一個呼叫 function 的用法,如此一來,我們知道自訂義的 myPromisify 要回傳一個 function。

1
2
3
4
5
const myPromisify = fn => {
return (...args) {

}
};

這邊的細節為在回傳的 function 的參數中,我們使用了展開餘數的方式做撰寫,因為,我們並不知道當外部在使用的時候會傳入多少的參數進來。

那 getSumPromise(1, 1) 後面是直接接了 chaining .then.catch ,由此可知我們剛剛在 myPromisify 裡面定義的那個回傳的 function ,它自身會回傳一個 Promise 物件,如此一來,才有辦法做 Promise chaining 的操作。

故再加上回傳 Promise 物件的內容,會變得像下面這樣

1
2
3
4
5
6
7
const myPromise = (fn) => {
return (...args) => {
return new Promise(resolv, reject) {
// executor
}
}
}

接下來,要如何得知是什麼時機點要呼叫回傳 Promise 的 resolve 和 reject 的?

這個時機點,就需要靠傳入被 promisify 的 function 的 callback function 來決定囉,寫法如下

1
2
3
4
5
6
7
8
9
10
const myPromise = (fn) => {
return (...args) => {
return new Promise((resolve, reject) => {
function callbackFnc(err, result) {
if(err) reject(err); // 有 err 傳入 callbackFnc 時,呼叫 reject
else resolve(result); // 有 result 傳入 callbackFnc 時,呼叫 resolve
}
})
}
}

但是,傳入 myPromise 的 function,本身並不知道 callbackFnc 的存在,所以,我們會透過將 callbackFnc 一併塞入 args 的做法,來讓傳入 myPromise 的 function,不僅獲得傳入參數的內容,同時,也可以使用 callbackFnc。

寫法如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const myPromise = (fn) => {
return (...args) => {
return new Promise(resolve, reject) {
function callbackFnc(err, script) {
if(err) reject(err);
else resolve(result);
}

args.push(callbackFnc);
fn.call(this, ...args);
}
}
}

const getSumAync = (num1, num2, callback) => {
if(!num1 || !num2) {
return callback(new Error('Missing arguments'), null);
}

return callback(null, num1 + num2);
}

const getSumAsync = myPromise(getSumAync);
getSumAsync(1, 2)
.then(res => console.log(res))
.catch(alert)

最後,我們加入了 [fn.call](http://fn.call)(this, ...args) 它的功用,
不僅是透過 fn.call 來執行傳入 myPromise 的 function 之外,
我們在 call 第一個傳入參數 this 是 window 物件,
第二個參數我們利用展開餘數,將傳入 args 展開,如此一來,它就會按照順序的為 getSumAync = (num1, num2, callback) 的三個參數依序賦值。

以上的步驟,就是撰寫我們自己的 promisify function 的流程。

若傳入的 callback function 會傳入多組結果的話怎麼辦?

也就是說 callbackFnc(err, result) 不再只有一個 result 參數,它會傳入多個像這樣 callbackFnc(err, result1, result2 …)

那我們就要做以下的改寫

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const myPromise = (fn) => {
return (...args) => {
return new Promise((resolve, reject) => {
function callbackFnc(err, ...results) {
if(err) reject(err);
else resolve(results.length === 1 ? results[0]:results);
}

args.push(callbackFnc);
fn.call(this, ...args);
})
}
}

const getSumAync = (num1, num2, callback) => {
if(!num1 || !num2) {
return callback(new Error('Missing arguments'), null);
}
const sum = num1 + num2;
const message = `Sum is ${sum}`
return callback(null, sum, message);
}

const getSumAsync = myPromise(getSumAync);
getSumAsync(1, 2)
.then(res => console.log(res)) // [3, "Sum is 3"]
.catch(alert)

以上就是拆解 promisify function 的經過囉~

Reference

  1. https://javascript.info/promisify
  2. 詳細拆解 promisify function 製作步驟的文章