跳至主要内容

[JS] Async and Await in JavaScript

解決 JS 中 Async 問題的第一個方法是用 callback function,但為了避免 callback hell 後來我們可以使用 Promise,在 ES2017 正式制訂了 asynchronous functions,可以讓我們在寫非同步碼的程式時,如同在寫同步的程式碼一般。

async function getData () {
let resolveValue = await <Promise>
}

程式碼範例:

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 處於 fulfilledrejected 狀態後才會執行。每一個 async function 都會回傳一個 promise

foo().then((res) => console.log(res)); // ‘success!’

async function foo() {
await somePromise();
return 'success!';
}

function somePromise() {
setTimeout(() => {
console.log('get data');
}, 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, whilefor...of 這種迴圈中都可以正常使用;但若是在帶有 callback 的迴圈中,例如 forEach, map, filter, reduce 等等,就需要特別留意:

  1. 如果想要在迴圈中使用 await,則可以使用一般的 for 迴圈、for await...of,或 map 搭配 Promise 的寫法,千萬不要使用 forEach
  2. 若有需要使用 filterreduce 等其他處理陣列的方式,先等陣列的資料完整取得後再來呼叫這些方法。

如果要「依序」拿到每個值

  • 方法一:使用 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);
}
})();

參考資料