[JS] Async and Await in JavaScript
解決 JS 中 Async 問題的第一個方法是用 callback function,但為了避免 callback hell 後來我們可以使用 Promise,在 ES2017 正式制訂了 asynchronous functions,可以讓我們在寫非同步碼的程式時,如同在寫同步的程式碼一般。
async function getData () {
let resolveValue = await <Promise>
}
程式碼範例:
- Promise 和 Async/Await @ PJCHENder Gist
- Demo Async and Await @ JSFiddle
Use Async Await with Promise
觀念
AsyncFunction 物件和 Generator 非常相似,兩者的差別在於 AsyncFunction 會回傳 Promise 物件而不是 { value: any, done: Boolean }。
Async Function
async function getData() {
// ...
}
當 async function 被呼叫的時候,它會回傳一個
Promise。當 async function 回傳一個值,該 Promise 狀態會是 resolved 且帶有該回傳值。當 async function 拋出一個例外或某個值,該 Promise 狀態會是 rejected 且帶有該拋出的值。async function 內部可以使用
await表達式,它會暫停此 async function 的執行,並且等待傳遞至表達式的 Promise 的解析,解析完之後會回傳解析值,並繼續此 async function 的執行。From :Async Function @ MDN
Await Function
/**
* await 會暫停 async function 的執行,
* 等待 Promise 物件被 resolve 後再繼續 async function 的執行。
* 同時回傳被 resolve 的值。
**/
async function getData () {
let resolveValue = await <Promise>
}
如果回傳值不是一個 Promise 物件,則會被轉換為 resolved 狀態的 Promise 物件。 如果 Promise 物件被 rejected,則 await 會丟出 rejected 的值。 From :Await @ MDN
// async function 本身會回傳 Promise,
// 因此 async function 之後也可以用 `.then()` 串起來
main().then((value) => {
console.log(value);
});
async function main() {
console.log('start fn'); // STEP 1-1
let val = await get(); // STEP 1-2
console.log('get value after await', val); // STEP 2-1
let result = await process(val); // STEP 2-2
console.log('result', result); // STEP 3-1
return result;
}
function get() {
console.log('I am in get out of Promise'); // STEP 1-3
return new Promise((resolve, reject) => {
// STEP 1-4
setTimeout(() => resolve('secret'), 1000); // STEP 1-5 To STEP 2 after 1 sec
});
}
function process(value) {
console.log("I'm in process,", value); // STEP 2-3
return new Promise((resolve, reject) => {
// STEP 2-4
setTimeout(() => resolve(`${value}-secret`), 1000); // STEP 2-5 To STEP 3 after 1 sec
});
}
基本寫法
我們只需要在定義 function 前加上 async 就可以使用 async function
// Basic function
async function foo() { … }
// Arrow function
const foo = async () => { … }
// Class methods
class Bar {
async foo() { … }
}
在我們定義好 async function 後,我們就可以使用 await 這個關鍵字,這個關鍵字會放在 Promise 之前,直到這個 promise 處於 fulfilled 或 rejected 狀態後才會執行。每一個 async function 都會回傳一個 promise。
foo().then((res) => console.log(res)); // ‘success!’
async function foo() {
await somePromise();
return 'success!';
}
function somePromise() {
return new Promise((resolve) => {
setTimeout(() => {
console.log('get data');
resolve();
}, 1000);
});
}
錯誤處理
在 async 和 await 中的錯誤處理是使用 try...catch...:
async function foo() {
try {
const value = await somePromise();
return value;
} catch (err) {
console.log('Oops, there was an error :(');
}
}
callback function
main(); // STEP 1-1
function main() {
console.log('start run'); // STEP 1-2
get((value) => {
// STEP 1-3
console.log('value in get', value); // STEP 2-1
process(value, function (result) {
// STEP 2-2
console.log(result); // STEP 3-2
});
});
}
function get(callback) {
console.log('run fn get'); // STEP 1-4
return setTimeout(function () {
// STEP 1-5 into STEP 2
callback('get data'); // STEP 2 會在兩秒後執行
}, 2000);
}
function process(value, callback) {
console.log('value in process', value); // STEP 2-3
return setTimeout(function () {
// STEP 2-4 in to STEP 3
callback(`${value}-secrete`); // STEP 3-1 會在等 1 秒(共 3 秒)後執行
}, 1000);
}
/**
* main 執行 -> // start run(0sec)
* get(callback) ->
* 兩秒後執行 get 裡的 callback
* - 1. console.log(value in get get data) // value in get get data (2sec)
* - 2. process(value, callback) // value in process get data (2sec)
* 一秒後執行 process 裡的 callback
* - 1. console.log(`${value}-secrete`) // get data-secrete (3sec)
**/
搭配 Promise.all 使用
/**
* Async-Await 搭配 Promise.all 使用
*/
doubleAndAdd(1, 2).then((value) => {
console.log(`doubleAndAdd(a, b)is ${value}`);
});
async function doubleAndAdd(a, b) {
const [aAfterDouble, bAfterDouble] = await Promise.all([
doubleAfterOneSecond(a),
doubleAfterOneSecond(b),
]);
console.log(
`a after double is ${aAfterDouble}. b after double is ${bAfterDouble}`
);
return aAfterDouble + bAfterDouble;
}
function doubleAfterOneSecond(params) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(params * 2);
}, 1000);
});
}
換成 Promise 的寫法
main();
function main() {
console.log('start fn'); // STEP 1-1
get() // STEP 1-2
.then(process) // STEP 2-1
.then((result) => console.log('result', result)); // STEP 3-1
}
function get() {
console.log('I am in get out of Promise'); // STEP 1-3
return new Promise((resolve, reject) => {
// STEP 1-4
setTimeout(() => resolve('secret'), 1000); // STEP 1-5 To Step 2 after 1 sec
});
}
function process(value) {
console.log("I'm in process,", value); // STEP 2-2
return new Promise((resolve, reject) => {
// STEP 2-3
setTimeout(() => resolve(`${value}-secret`), 1000); // STEP 2-4 To STEP 3 after 1 sec
});
}
從 Promise 到 async/await
From Promise to async/await @ Repl.it
掘竅(Tips)
在迴圈中使用 async/await
若想要在迴圈中使用 async / await 的寫法需要特別留意, 一般來說在 for, while 或 for...of 這種迴圈中都可以正常使用;但若是在帶有 callback 的迴圈中,例如 forEach, map, filter, reduce 等等,就需要特別留意:
- 如果想要在迴圈中使用
await,則可以使用一般的for迴圈、for await...of,或map搭配Promise的寫法,千萬不要使用forEach。 - 若有需要使用
filter或reduce等其他處理陣列的方式,先等陣列的資料完整取得後再來呼叫這些方法。
如果要「依序」拿到每個值
- 方法一:使用
for await(const item of promises) {...},每個 Promise 結束後可以馬上取得結果 - 方法二:使用 for...of,每個 Promise 結束後可以馬上取得結果
- 方法三:使用 for 迴圈,每個 Promise 結束後可以馬上取得結果
要一次取得所有結果
- 方法四,使用 Promise.all,一次取得所有結果,需等待所有 Promise resolve 或其中一個被 reject 時終止
- 方法五,使用 Promise.allSettled,一次取得所有結果,需等待所有 Pormise 都 resolve(fulfilled) / reject(rejected) 後終止
- 方法六:使用 for...of await,一次取得所有結果,需等待所有 Promise resolve 或其中一個被 reject 時終止(不如就用 Promise.all 吧)
const sleep = (ms) => {
return new Promise((resolve, reject) => {
console.log(`[sleep-${ms}] invoke`);
setTimeout(() => {
console.log(`[sleep-${ms}] resolve`);
// if (ms === 2000) {
// reject(ms)
// return
// }
resolve(ms);
}, ms);
});
};
// 所有的 Promise 會在此時就發動
const promises = [sleep(1000), sleep(3000), sleep(2000)];
// 方法一:使用 for await...of,每個 Promise 結束後可以馬上取得結果
(async function () {
for await (const item of promises) {
console.log('[for await...of] result', item);
}
})();
// 方法二:使用 for...of,每個 Promise 結束後可以馬上取得結果
(async function () {
for (const item of promises) {
const result = await item;
console.log('[for...of] result', result);
}
})();
// 方法三:使用 for 迴圈,每個 Promise 結束後可以馬上取得結果
(async function () {
for (let i = 0; i < promises.length; i++) {
const result = await promises[i];
console.log('[for] result', result);
}
})();
// 方法四,使用 Promise.all,一次取得所有結果,需等待所有 Promise resolve 或其中一個被 reject 時終止
(async function () {
const results = await Promise.all(promises);
console.log('[Promise.all] results', results);
})();
// 方法五,使用 Promise.allSettled,一次取得所有結果,需等待所有 Pormise 都 resolve(fulfilled) / reject(rejected) 後終止
(async function () {
const results = await Promise.allSettled(promises);
console.log('[Promise.allSettled] results', results);
})();
// 方法六:使用 for...of await,一次取得所有結果,需等待所有 Promise resolve 或其中一個被 reject 時終止(不如就用 Promise.all 吧)
(async function () {
for (const result of await Promise.all(promises)) {
console.log('[for...of await] result', result);
}
})();
參考資料
- Async Function @ MDN
- Await @ MDN
- Asynchronous Functions 101 @ bitsofcode
- The easiest guide to understand async and await in Javascript @ youtube
- JavaScript 中回調地獄的今生前世 @ 碼衣網