使用 Promise
Promise
是一個表示非同步運算的最終完成或失敗的物件。由於多數人使用預建立的 Promise,這個導覽會先講解回傳 Promise 的使用方式,之後再介紹如何建立。
基本上,一個 Promise 是一個根據附加給他的 Callback 回傳的物件,以取代傳遞 Callback 到這個函數。
舉例來說,下方的範例若用舊方式應該會有兩個 Callback,並根據成功或失敗來決定使用哪個:
function successCallback(result) {
console.log("It succeeded with " + result);
}
function failureCallback(error) {
console.log("It failed with " + error);
}
doSomething(successCallback, failureCallback);
而新作法會回傳一個 Promise,這樣你就可以附加 Callback:
let promise = doSomething();
promise.then(successCallback, failureCallback);
再簡單點:
doSomething().then(successCallback, failureCallback);
我們稱之為 非同步函數呼叫。這個做法有許多好處,我們接下來看看。
保證
不如舊做法,一個 Promise 有這些保證:
- Callback 不會在當次的迴圈運行結束前呼叫。
- Callback 用 .then 添加,在非同步運算結束後呼叫,像前面那樣。
- 複 Callback 可以透過重複呼叫 .then 達成。
但 Promise 主要的立即好處是串連。
串連
有個常見的需求是依序呼叫兩個以上的非同步函數,我們稱之為建立 Promise 鏈。
看看魔術:then
函數回傳一個新的 Promise,不同於原本。
let promise = doSomething();
let promise2 = promise.then(successCallback, failureCallback);
或
let promise2 = doSomething().then(successCallback, failureCallback);
第二個 Promise 不只代表 doSomething()
完成,還有successCallback
或 failureCallback
,這兩個非同步函數回傳另一個 Promise。如此一來,任何 Callback 附加給 promise2
會被排在 successCallback
或failureCallback
之後。
基本上,每個 Promise 代表著鏈中另外一個非同步函數的完成。
在古時候,多個非同步函數會使用 Callback 方式,導致波動拳問題:
doSomething(function (result) {
doSomethingElse(
result,
function (newResult) {
doThirdThing(
newResult,
function (finalResult) {
console.log("Got the final result: " + finalResult);
},
failureCallback,
);
},
failureCallback,
);
}, failureCallback);
有了新方法,我們附加 Callback 到回傳的 Promise 上,來製造 Promise 鏈:
doSomething()
.then(function (result) {
return doSomethingElse(result);
})
.then(function (newResult) {
return doThirdThing(newResult);
})
.then(function (finalResult) {
console.log("Got the final result: " + finalResult);
})
.catch(failureCallback);
then
的函數是選用的,以及 catch(failureCallback)
是 then(null, failureCallback)
的簡寫。你也許會想用箭頭函數取代:
doSomething()
.then((result) => doSomethingElse(result))
.then((newResult) => doThirdThing(newResult))
.then((finalResult) => {
console.log(`Got the final result: ${finalResult}`);
})
.catch(failureCallback);
注意:永遠要回傳結果,否則 Callback 不會獲得前一個 Promise 的結果。
獲錯後串接
失敗後的串接是可行的,也就是說 catch
會非常好用,即使鏈中出錯。看看這個範例:
new Promise((resolve, reject) => {
console.log("Initial");
resolve();
})
.then(() => {
throw new Error("Something failed");
console.log("Do this");
})
.catch(() => {
console.log("Do that");
})
.then(() => {
console.log("Do this whatever happened before");
});
他會輸出:
Initial Do that Do this whatever happened before
注意「Do this」沒有被輸出,因為「Something failed」錯誤導致拒絕。
錯誤傳遞
在波動拳狀況中,你可能會看到三次 failureCallback
,在 Promise 鏈中只需要在尾端使用一次:
doSomething()
.then((result) => doSomethingElse(result))
.then((newResult) => doThirdThing(newResult))
.then((finalResult) => console.log(`Got the final result: ${finalResult}`))
.catch(failureCallback);
基本上,一個 Promise 鏈遇到錯誤時會往下尋找 Catch 處理器。這是經過模組化的非同步程式:
try {
let result = syncDoSomething();
let newResult = syncDoSomethingElse(result);
let finalResult = syncDoThirdThing(newResult);
console.log(`Got the final result: ${finalResult}`);
} catch (error) {
failureCallback(error);
}
在 ECMAScript 2017 中,在有 async
/await
語法糖的同步程式達到高峰:
async function foo() {
try {
let result = await doSomething();
let newResult = await doSomethingElse(result);
let finalResult = await doThirdThing(newResult);
console.log(`Got the final result: ${finalResult}`);
} catch (error) {
failureCallback(error);
}
}
這基於 Promise,例如 doSomething()
和之前一樣。你可以閱讀在這裡閱讀更多。
Promise 藉由捕捉所有錯誤,包含例外和程式錯誤,解決了 Callback 地獄的缺點。這是非同步運算的基本特性。
在舊有 API 上建立 Promise
Promise
可以透過建構子建立。所以用建構子包裹舊的 API 即可。
在理想情況,所有非同步函數都會回傳 Promise,然而許多 API 仍然用舊的方式來傳遞成功、失敗 Callback,有個典型的例子是setTimeout()
:
setTimeout(() => saySomething("10 seconds passed"), 10000);
混合古代 Callback 和 Promise 是有問題的。如果 saySomething
失敗或有程式錯誤,那不會有任何錯誤被捕捉。
幸運地,我們可以用 Promise 包裹他,最好盡可能的在最底層包裹,並永遠不要再直接呼叫他們:
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
wait(10000)
.then(() => saySomething("10 seconds"))
.catch(failureCallback);
基本上,Promise 建構子需要一個運作函數來正規地處理或拒絕 Promise。但因為 setTimeout
不會失敗,所以我們捨棄 reject。
組合
Promise.resolve()
和 Promise.reject()
是用來正規地建立已經處理或拒絕的 Promise。他們在某些情況特別有用。
Promise.all()
和 Promise.race()
是兩個組合工具用於使 Promise 平行運作。
連續關聯是可行的,這是極簡 JavaScript 範例:
[func1, func2].reduce((p, f) => p.then(f), Promise.resolve());
基本上,我們摺疊(Reduce)一個非同步函數陣列成一個 Promise 鏈:Promise.resolve().then(func1).then(func2);
這可以用可重用的構成函數完成,通常用函數式編程:
let applyAsync = (acc, val) => acc.then(val);
let composeAsync =
(...funcs) =>
(x) =>
funcs.reduce(applyAsync, Promise.resolve(x));
composeAsync
接受任何數量的函數作為參數,並回傳一個接受一個初始值用來傳給組合的新函數。這個好處是無論其中函數是非同步或否,都會保證用正確的順序執行:
let transformData = composeAsync(func1, asyncFunc1, asyncFunc2, func2);
transformData(data);
ECMAScript 2017 中連續組合利用 async/await 更簡單:
for (let f of [func1, func2]) {
await f();
}
計時
為了避免意外,傳給 then
的函數不會被同步地呼叫,即使是完成的 Promise:
Promise.resolve().then(() => console.log(2));
console.log(1); // 1, 2
被傳入的函數會被放在子任務佇列而非立即執行,因此他會在當前的事件迴圈結束、佇列清空時執行,例如:
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
wait().then(() => console.log(4));
Promise.resolve()
.then(() => console.log(2))
.then(() => console.log(3));
console.log(1); // 1, 2, 3, 4