在異步的處理上,我們從最初的 callback
(副作用:回呼地獄),到後來的 Promise
(解決回呼地獄,鏈式呼叫),以及上篇的 Generator
+ Promise
(讓異步呼叫處理上能更接近同步風格)。
直到了 ES 2017,又推出了新的處理異步的方法:async
和 await
。
Generator
+ Promise
儘管語法使用上已經比前幾個版本優化了不少,但對使用者來說需要理解的內容較多較複雜,async
和 await
這組語法更簡潔,甚至你不需去理解背後的機制,只需理解這兩個語法糖如何使用,就能夠用近似同步風格的程式碼來處理異步操作。
這兩個語法也是基於 Promise
衍生的。
async
寫於一個異步函式的定義之前。被加上 async
關鍵字的函式,即使沒有任何返回值,也會預設返回一個 Promise
物件。
如果該函式有返回值,則會將返回值存在 Promise
物件中;如果拋出例外,則 Promise
物件的狀態會是 reject
。
async function foo(input) {
if(typeof input === "undefined")
return 'bar!';
else
throw Error("Don't put anything in the ()!");
}
console.log(foo().toString());//"[object Promise]"
console.log(foo() instanceof Promise);//true
foo().then(x=>{console.log(x)});//"bar!"
foo("garbage")
.then(x=>{console.log(`Fulfill:${x}`)})
.catch(x=>{console.log(`Reject:${x}`)});//"Reject:Error: Don't put anything in the ()!"
如上所說,即使裡面根本沒有特定的異步操作,加上 async
後就會使該函式回傳一個 Promise
物件,這個例子也展示了上面提到的回傳值與例外的情況。
await
只能使用於 async 函式內部,用於等待一個 Promise
被解決(Settle),並獲取返回的值。
在被解決前,程式碼會在該處進行等待直至得到返回值。
async function cookEgg(waterAmount=0) {
console.log("開始煮水");
try {
await boilWater(waterAmount);
console.log("可以來煮蛋囉!");
} catch (error) {
console.log("發生錯誤:", error);
}
}
function boilWater(waterAmount) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (waterAmount < 1) {
reject("水量太少");
} else {
console.log("水煮開了");
resolve();
}
}, 1000);
});
}
cookEgg(2);
boilWater
是一個異步操作的函式,回傳了一個 Promise
物件,在外面調用的時候使用了 await
關鍵字來等待煮水的結果,確定煮完才能繼續往下一步前進。
也因為了 await
,所以 cookEgg
函式需要加上 async
關鍵字。
await
僅能使用於 async
內部的原因是因為 async
語法提供了異步的上下文,這是一般函式中沒有的(一般函式由上而下執行,,無法暫停,無法等待異步),透過 async
提供的上下文,得以實現等待 await
的行為:暫停與恢復,同時 async
也能明確標示這個函式是一個異步函式,避免同步異步混用,而不易辨別流程順序的情況。
與生成器(Generator)相比,不只語法變得簡潔,錯誤處理也更容易編寫。
生成器中的 try catch
必須寫在生成器裡面,若外部發生錯誤,則須對返回的生成器物件使用 .throw()
來觸發例外處理。
async
與 await
則可以在 async
函式裡面直接用 try catch
捕捉 await
行執行函式回傳的 Promise
物件為拒絕(reject)時的情形,不再需要在自己異步函式內外雙邊傳遞。
因為 await
會暫停並等待異步函式的回傳,所以當你需要一次呼叫多個異步的時候你沒辦法寫成這樣:
async function foo(){
await bar1();
await bar2();
}
這樣的執行順序會變成等待 bar1()
執行完後才接著執行 bar2()
。
為了處理這個情境,Promise
其實還有個語法可以使用:Promise.all
。Promise.all
會接受一個帶有多個 Promise
物件的可迭代物件(如陣列),返回一個單一的 Promise
物件。
async function foo(){
const [bar1Result, bar2Result] = await Promise.all([bar1(), bar2()]);
}
被回傳的 Promise
物件會在傳入的所有 Promise
物件被 resolve
時回傳 resolve
,值會是一個陣列,對應到傳入可迭代物件的各個 Promise
物件。
當然,因為回傳的是一個 Promise
物件,能和 async
await
一起使用。
async function foo() {
try{
const promise1 = new Promise(resolve => setTimeout(() => resolve("bar1"), 100));
const promise2 = new Promise(resolve => setTimeout(() => resolve("bar2"), 200));
const promise3 = new Promise(resolve => setTimeout(() => resolve("bar3"), 50));
const results = await Promise.all([promise1, promise2, promise3]);
console.log(results);//["bar1", "bar2", "bar3"]
}
catch(error){
console.log(error);
}
}
foo();
另外,不管 Promise
的完成順序,Promise.all
的回傳是基於呼叫的順序,像上面的例子,理論上完成順序是 3,1,2,但 Promise.all
收到的仍是 1,2,3。
要注意的是,如果其中任一個 Promise
收到 reject
,會立刻終止這次的 Promise.all
,並回傳該收到 reject
的值。甚至比較早完成的 Promise
的回傳值也都不會顯示,僅會顯示該次 reject
的值。
async function foo() {
try{
const promise1 = new Promise(resolve => setTimeout(() => resolve("bar1"), 100));
const promise2 = new Promise((resolve, reject) => setTimeout(() => reject("fail"), 200));
const promise3 = new Promise(resolve => setTimeout(() => resolve("bar3"), 50));
const results = await Promise.all([promise1, promise2, promise3]);
console.log(results);
}
catch(error){
console.log(error);
}
}
foo();
聽起來是不是有點危險?
沒關係,時間的力量是偉大的,Promise.all
都已經是 ES6 的語法了 -- 在 ES 2020,引入了另一個方法 Promise.allSettled
。
這個方法使用上和 Promise.all
一模一樣,只差在回傳的邏輯。
這個方法無論中間哪個 Promise
被 reject
了,也不會影響對其他 Promise
的等待,回傳的時機點是所有的 Promise
都被 settle
後。
即使中間有 reject
,Promise.allSettled
本身的回傳依然是一個 resolve
的 Promise
。
async function foo() {
try{
const promise1 = new Promise(resolve => setTimeout(() => resolve("bar1"), 100));
const promise2 = new Promise((resolve, reject) => setTimeout(() => reject("fail"), 200));
const promise3 = new Promise(resolve => setTimeout(() => resolve("bar3"), 50));
const results = await Promise.allSettled([promise1, promise2, promise3]);
console.log("In try", results);
// "In try",
// [{
// status: "fulfilled",
// value: "bar1"
// },{
// reason: "fail",
// status: "rejected"
// }, {
// status: "fulfilled",
// value: "bar3"
// }]
}
catch(error){
console.log("In catch", error);
}
}
foo();
可以注意到他的回傳結構再稍微複雜一點,針對狀態是 reject
的 Promise
,是一個 {reason: status:}
的物件,針對狀態是 resolve
的 Promsie
,則是 {value:,status:}
的物件。
可以用 status
屬性來辨別是否該 Promise
是成功的。
異步相關的主題到此告一個段落,目前異步的主要處理方式多為使用 async
await
來做流程控制,優點是語法更為簡潔,容易處理例外。
從前面幾篇到這裡callback
-> Promsie
-> Promise + Generator
-> async + await
,逐一介紹各個概念後相信會更清楚自己用的語法的特點與限制,寫起來會更有信心。