昨天聊了 callback 與 Promise,是如何過關斬將,不斷克服障礙走到 ES6。
然而只要程式規模不斷擴張,就永遠會有更高階的需求產生,讓我們繼續來看,Promise 之後發生了什麼故事吧!
ES6 Promise 已經是滿不錯的設計,只是它長得還是太像非同步了(?),如果我有資料仰賴非同步取回,就會變成同步跟非同步兩個大區塊,但 ES7 新登場的 async/await,開始漸漸模糊了同步與非同步的界線。
async/await 是 ES7 版本的一個 Promise 語法糖,透過 async
跟 await
兩個關鍵字,可以將原本執行多行 Promise 程式簡化成一行,並且使用方式非常貼近一般的同步程式碼,大幅提高程式的可讀性。
async
關鍵字放在 function 的前面,代表「宣告一個非同步的函式」await
關鍵字放在呼叫 async function 的前面,代表「呼叫並等待這個非同步函式」async
跟 await
是成對出現的async function 其實也是一種物件種類,有興趣可以查看 Mozilla MDN
// 用 setTimeout 模擬一個要等 3 秒的 Promise
const wait3Seconds = async (x) => {
return new Promise(resolve => {
setTimeout(() => {
resolve(x);
}, 3000);
});
}
// 函式先 await 再賦值
const a = await wait3Seconds(1);
const b = await wait3Seconds(2);
const sum = a + b;
console.log(sum); // 3
// 函式先賦值再 await
const c = wait3Seconds(1);
const d = wait3Seconds(2);
const sum2 = await c + await d;
console.log(sum2); // 3
有注意到上面的例子中,await 的擺放位置不同嗎?可是執行出來的 sum 跟 sum2 結果都一樣耶,所以 await 放哪很重要嗎?
好奇的話不妨複製貼到 console 跑跑看,記得分兩段跑,1~14 行先跑,看完結果再跑 17~20 行。
先公布答案:
第一段要等 6 秒才會有結果,而第二段只要等 3 秒。為什麼呢?
新手如果不熟,不妨這樣記憶:看到 await
代表要等。
第一段 (函式先 await
再賦值):
await
所以要等,setTimeout
被啟動開始倒數a
await
所以要等,setTimeout
被啟動開始倒數b
第二段 (函式先賦值再 await
):
await
所以不等,把 Promise 物件賦值給 c
,setTimeout
被啟動開始倒數await
所以不等,把 Promise 物件賦值給 d
,setTimeout
被啟動開始倒數await
所以要等 c
完成await
所以要等 d
完成,但剛剛那 3 秒已經讓 d
也完成了,所以相加之後印出另一個值得思考的點是,因為 await
的順序不同,導致 a
、b
這兩個變數存的是 number
,而 c
、d
則存了 Promise
物件,有興趣不妨印出來看看。
簡化了 Promise 複雜的結構,讓非同步函式可以變得像是同步一樣,不僅可讀性更高,在 error handling 的方面,也可以使用既有的 try catch
來解決,某種程度上也讓新手更容易上手。
如同昨天 Promise 的例子,同步/非同步的界線非常鮮明,要改動常常要顧慮這一行到底是同步還非同步,但改用 async/await 之後,看起來全都像是同步程式碼:
const doQueryWallet = async () => {
// 這裡都跟原本一樣,只是多了 async
};
const doOrder = async () => {
// 這裡都跟原本一樣,只是多了 async
};
const wallet = await doQueryWallet();
if (wallet.balance > 0) {
const order = await doOrder();
if (order.status === 'ok') {
console.log('下單成功');
} else {
console.log('下單失敗');
}
} else {
console.log('餘額不足');
}
當然 error handling 的部分,也可以用熟悉的 try catch
包起來:
try {
const wallet = await doQueryWallet();
if (wallet.balance > 0) {
const order = await doOrder();
if (order.status === 'ok') {
console.log('下單成功');
} else {
console.log('下單失敗');
}
} else {
console.log('餘額不足');
}
} catch (err) {
console.error(err);
}
以上講的 case,大部分都還是基於「由上而下」的順序,也就是我們很熟悉同步的程式碼順序,一個一個由上而下執行,第二行絕對會等第一行結束才執行。
非同步有以下三種常見的順序,可以透過 async/await 及 Promise 簡單達到以下效果:
這是我們最常見的順序,A 執行完換 B,B執行完換 C,後者不能早於前者,通常是因為後者依賴於前者的資料。
比如:一定要先查完帳戶餘額,確定有餘額才能夠下單。
const sequence = async () => {
const output1 = await a();
const output2 = await b();
const output3 = await c();
return `sequence is done ${output1} ${output2} ${output3}`;
}
成功的情況
a
|--------|
b
|--------|
c
|--------|
非同步結束
|----------
拒絕的情況
a
|--------|
b
|--------X
非同步結束(throw error)
|--------
這是一個比較有效率的執行方式,不管順序,把所有非同步程式都撒出去執行,等全部都完成(fulfilled
)再一次告訴我。通常代表這幾個非同步函式沒有互相依賴的資料,可以同時執行。
比如:查詢店家評論、查詢我的優惠券、查詢帳戶餘額,三者沒有互相依賴的資料,但是會出現在同一個頁面,因此把三個函式都發出去,全部回來再一次做接下來的動作(比如把 loading 圖示關掉)。
const parallel = async () => {
const [output1, output2, output3] = await Promise.all([a(), b(), c()]);
return `parallel is done ${output1} ${output2} ${output3}`;
}
成功的情況
a
|-----|
b
|--------------|
c
|--------|
非同步結束,取得回傳結果
|----------
拒絕的情況
a
|-----X
b
|--------------|
c
|--------|
非同步結束,取得回傳錯誤
|----------
注意,成功與失敗的回傳值是不同的,成功時把結果包成一個 array 回傳,裡頭的順序即
Promise.all
裡面的順序。而拒絕則是任何一個 Promise 拒絕(reject
)就會立刻回傳最早拒絕的結果。
注意,Promise 是不可打斷的,發出去的函式跟潑出去的水一樣,唯一能做的,就是忽略回傳值,所以即便上圖 a 最早就被拒絕了,b 跟 c 還是會把他們該做的事情在背景跑完,只是我們不會收到結果就是了。
這算是 parallel 的一個變種(?),也是把所有非同步程式都撒出去執行,但就像賽跑一樣,只要任何一個完成(fulfilled
)或拒絕(reject
),就立刻回傳告訴我,其他未完成的則直接忽略,比較是運用在較特殊的情境。
比如:可以幫 request 設定一個 timeout 的時間限制,放一個 setTimout 10 秒就會 reject
的 Promise 進去 Promise.race
,就會強制在 10 秒之內得到結果。
成功的情況
a
|-----|
b
|--------------|
c
|--------|
非同步結束,取得回傳結果
|----------
拒絕的情況
a
|-----X
b
|--------------|
c
|--------|
非同步結束,取得回傳錯誤
|----------
const race = async () => {
const output1 = await Promise.race([a(), b(), c()]);
return `race is done ${output1}`;
}
非同步很複雜,但也就是因為很複雜,可以玩的花樣更多,許多複雜的需求其實都是建構在非同步程式中,否則我們的程式就永遠只能同步一行一行執行,似乎也少了一些醍醐味。
無論如何看待
我終將完成我的
承諾
JAVACRIPT.INFO: Promises, async/await
MDN - Promise.all
MDN - Promise.race
不確定自己對第一個範例理解有不足,試著去執行第一個範例,出現以下的錯誤
await is only valid in async functions and the top level bodies of modules
所以把第一個範例做了一點修正,更新後如下
const wait3Seconds = (x) => {
return new Promise(resolve => {
setTimeout(() => {
resolve(x);
}, 3000);
});
}
// 函式先 await 再賦值
async function f(){
const a = await wait3Seconds(1);
const b = await wait3Seconds(2);
const sum = a + b;
console.log(sum);
// const c = wait3Seconds(1);
// const d = wait3Seconds(2);
// const sum2 = await c + await d;
// console.log(sum2);
}
f()
結果就可以正確運行,不確定是否理解正確,回傳Promise物件的function不需要async關鍵字,使用await時,需要有async function將其包起來。