「承諾化」是一個很長的字,用來表示一個簡單的轉換。它將接受回呼的函式轉換成回傳承諾的函式。
在現實生活中,通常需要進行這樣的轉換,因為許多函式和函式庫都是基於回呼的。但是承諾比較方便,所以將它們承諾化是有意義的。
為了更深入地了解,我們來看一個範例。
例如,我們有章節 簡介:回呼 中的 loadScript(src, callback)
。
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}`));
document.head.append(script);
}
// usage:
// loadScript('path/script.js', (err, script) => {...})
此函式會載入具有給定 src
的腳本,然後在發生錯誤時呼叫 callback(err)
,或是在載入成功時呼叫 callback(null, script)
。這是使用回呼的廣泛共識,我們之前看過。
讓我們將它承諾化。
我們將建立一個新的函式 loadScriptPromise(src)
,它執行相同的動作(載入腳本),但會回傳承諾,而不是使用回呼。
換句話說,我們只傳遞 src
(沒有 callback
)給它,並取得一個承諾作為回傳,在載入成功時會以 script
解決,否則會以錯誤拒絕。
在這裡
let loadScriptPromise = function(src) {
return new Promise((resolve, reject) => {
loadScript(src, (err, script) => {
if (err) reject(err);
else resolve(script);
});
});
};
// usage:
// loadScriptPromise('path/script.js').then(...)
正如我們所見,新函式是原始 loadScript
函式的包裝器。它呼叫它,提供它自己的回呼,轉換為承諾 resolve/reject
。
現在 loadScriptPromise
非常適合基於承諾的程式碼。如果我們比回呼更喜歡承諾(我們很快就會看到更多原因),那麼我們將使用它。
在實務上,我們可能需要將多個函式承諾化,因此使用輔助程式是有意義的。
我們將呼叫它 promisify(f)
:它接受要承諾化的函式 f
,並傳回包裝函式。
function promisify(f) {
return function (...args) { // return a wrapper-function (*)
return new Promise((resolve, reject) => {
function callback(err, result) { // our custom callback for f (**)
if (err) {
reject(err);
} else {
resolve(result);
}
}
args.push(callback); // append our custom callback to the end of f arguments
f.call(this, ...args); // call the original function
});
};
}
// usage:
let loadScriptPromise = promisify(loadScript);
loadScriptPromise(...).then(...);
程式碼看起來可能有點複雜,但它基本上與我們在上面承諾化 loadScript
函式時寫的一樣。
呼叫 promisify(f)
會傳回 f
(*)
的包裝器。該包裝器傳回承諾,並將呼叫轉發到原始 f
,在自訂回呼中追蹤結果 (**)
。
在這裡,promisify
假設原始函式預期回呼有兩個參數 (err, result)
。這是我們最常遇到的情況。然後,我們的自訂回呼格式完全正確,而 promisify
非常適合這種情況。
但是,如果原始 f
預期回呼有更多參數 callback(err, res1, res2, ...)
呢?
我們可以改善我們的輔助程式。讓我們製作更進階版本的 promisify
。
- 當呼叫為
promisify(f)
時,它應與上述版本類似。 - 當呼叫為
promisify(f, true)
時,它應傳回承諾,並以回呼結果陣列解析。這完全適用於具有多個參數的回呼。
// promisify(f, true) to get array of results
function promisify(f, manyArgs = false) {
return function (...args) {
return new Promise((resolve, reject) => {
function callback(err, ...results) { // our custom callback for f
if (err) {
reject(err);
} else {
// resolve with all callback results if manyArgs is specified
resolve(manyArgs ? results : results[0]);
}
}
args.push(callback);
f.call(this, ...args);
});
};
}
// usage:
f = promisify(f, true);
f(...).then(arrayOfResults => ..., err => ...);
正如您所見,它基本上與上述相同,但 resolve
僅呼叫一個或所有參數,具體取決於 manyArgs
是否為真。
對於更特別的回呼格式,例如完全沒有 err
的格式:callback(result)
,我們可以手動承諾化此類函式,而不需要使用輔助程式。
還有一些模組具有更靈活的承諾化函式,例如 es6-promisify。在 Node.js 中,有一個內建的 util.promisify
函式。
承諾化是一個很棒的方法,特別是當您使用 async/await
(稍後在章節 Async/await 中介紹)時,但並非完全取代回呼。
請記住,承諾可能只有一個結果,但回呼技術上可以呼叫多次。
所以 promisification 僅適用於呼叫一次 callback 的函式。後續呼叫將會被忽略。
留言
<code>
標籤,對於多行程式碼 – 請用<pre>
標籤包覆,對於超過 10 行的程式碼 – 請使用沙盒 (plnkr、jsbin、codepen…)