0%

Promise(VIII) - Async/await

本文開始

撰寫有關 Promise 的程式碼有另外一個更輕鬆的寫法就是用 async/await 來撰寫。

Async function

如果,我們在一個 function 前面加上 async 這個關鍵字,
a. 就代表了這個 function 一定會回傳 Promise。

b. 如果,是回傳其他不是 Promise 類型的值得話,就會自動被包裹成 resolved Promise 的樣子。

來一些簡單的範例

以下這個範例,就是會隱性的被 async 將回傳值包裹成 resolved Promise 的範例

1
2
3
4
5
async function implicitFnc() {
return 1;
}

implicitFnc().then(res => console.log(res)); // 1

以下這個範例,就是顯性的撰寫回傳 resolved Promise 的範例

1
2
3
4
5
async function explicitFnc() {
return Promise.resolve('This is a result');
}

explicitFnc().then(res => console.log(res)); // 'This is a result'

Await

await 的特性就是

  1. 它只有在 async 的 function 底下有作用
  2. 它會等到跟它對應的 Promise 完成之後,才往下進行後面的程式碼,並且它會接收到由其對應的 Promise 回傳後的結果。

來個範例

1
2
3
4
5
6
7
8
9
async function fnc() {
const promise = new Promise((resolve, reject) => {
setTimeout(() => resolve("Done!!!!"), 3000)
})
const result = await promise; // step 1 - 等到 promise 結束為止,才往下執行
alert(result);
}

fnc();

上面的範例,
程式碼會在 step 1 這邊停住,直到 promise 完成之後,才往下執行,並且 promise 回傳的結果,會存在 result 裡。

雖然,fnc 這個 function 會暫停在 step 1 這一行,直到 promise 完成執行為止,但這並不代表整個 JS 都會停在這一行程式碼不執行,它還是一樣可以同一時間去執行其他的任務內容的。

Notice:
這邊再次強調, await 只有在 async 的 function 底下有作用。
如果你在不是 async 的 function 底下使用 await 是會報錯的優。
試試下面這個範例

function fnc() {
const promise = Promise.then(1);
const result = await promise; // error
}

fnc();

直接用 async…await 來改寫一下在 Promise chaining 的範例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
async function fnc() {
const response = await fetch('https://randomuser.me/api/');
const responseJson = await response.json();
const userInfo = responseJson.results[0];
const img = document.createElement('img');
img.src = userInfo.picture.large;
document.body.append(img);

const name = await new Promise((resolve, reject) => {
setTimeout(() => {
const userName = Object.keys(userInfo.name).reduce((acc, crr) => {
return acc += userInfo.name[`${crr}`] + ' ';
}, '');

resolve(userName);
}, 3000);
})

img.remove();
alert(`Showing ${name}`);
}

fnc();

Allow top-level await in modules

在現代的瀏覽器是允許 await 關鍵字出現在 modules 的上方出現的喔。
舉下面的範例

1
2
3
const enLangFile = fetch('../assets/i18n/en.json').then(res => res.json());

export default await enLangFile; // await enLangFile resolove

上面這個範例,只要有其他地方引入這個模組,都會先等 enLangFile 這個 Promise 解析完成之後,在繼續執行後續的內容。

感覺蠻方便的?

但是,若是比較舊版本的瀏覽器就沒有辦法讓我們這樣直接加 await 在 modules 上面的。
但是,我們可以改用下面的寫法,會有一樣的效果

1
2
3
4
5
--- 改用 IIFE 來改寫 ---
(async() => {
const enLangFile = fetch('../assets/i18n/en.json').then(res => res.json());
export default await enLangFile; // await enLangFile resolove
})()

await accepts Thenable

await 也可以用在某些本身具有 then method 的物件喔。
就是在 Promise chaining 那邊有提到,在某些第三方套件會用這樣的手法,來讓我們可以使用 Promise chaining 的效果。

舉個範例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Thenable {
constructor(num) {
this.num = num;
}

then(resolve, reject) {
setTimeout(() => resolve(this.num), 1000); // 在一秒後 resolve its num value
}
}

async function fnc() {
const result = await new Thenable(2);
alert(result);
}

fnc();

上面範例的 await 就會得到一個帶有 .then 方法的 non-Promise 的物件,await 會直接去呼叫該 object 的 .then 方法(這個方法將 JS 內建的 resovlereject 當成其參數,就像 Promise 那樣)。
接著,await 就會等到 resolvereject 其中一個被呼叫,並解出回傳出來的結果。

在 class 裡面定義 async method

若要在 class 中定義 async 方法,就直接在該方法名稱前面加上 async 即可。

1
2
3
4
5
6
7
8
9
class Waiter {
async wait() {
return await Promise.resolve(2);
}
}

new Waiter()
.wait()
.then(res => console.log(res)); // 2

上面的範例,因為 async 會自動地將不是 Promise 型別的結果,包裹成 Promise 型別並回傳,所以,透過 await 解析出來的結果,會透過 async implicitly transform into Promise type.

那最後,我們再用 .then 來接由該 async 方法回傳的結果。

Error handling

當我們用 await 來接一個 呼叫 reject 的 Promise。
await 會隱性地 throw the error.

1
2
3
async function f() {
await Promise.reject(new Error("This is an error."));
}

上面的程式碼等同於用下面這樣寫

1
2
3
async function f() {
throw new Error("This is an error.");
}

那也因為 await 有以上這樣的特性,我們可以搭配 try…catch 來做 error handling mechanism.

1
2
3
4
5
6
7
8
9
10
async function f() {
try {
const result = await fetch("http://no-such-url");
}
catch(err) {
alert(err);
}
}

f();

上面的範例, catch 會接到由 await 丟出來的 error。

以上的範例,有可以改由用 .catch 來承接丟出來的錯誤。

1
2
3
4
5
6
async function f() {
const response = await fetch('http://no-suc-url');
}

// f() becomes a rejected promise
f().catch(alert);

那如果丟出來的錯誤,沒有後續的人來承接的話,我們也可以用 unhandledrejection 這個 eventListener 來承接。

同時寫兩個錯誤的範例,第一個出現 error 的部分,會被丟到 .catch 裡,
以下面的範例 step 1 會被執行而且丟出錯誤,而 step 2 就不會被執行,
catch 區域接到的錯誤就是 step 1 丟出的錯誤。

1
2
3
4
5
6
7
8
9
10
11
12
13
async function f() {

try {
let response = await fetch('https://rxxxuser.me/api/?seed=1'); // step 1 丟出錯誤
let json = await response.json(); // step 2 不會被執行

} catch(err) {
// catches errors both in fetch and response.json
alert(err);
}
}

f();

await 搭配 Promise.all 一起使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const urls = [
'https://randomuser.me/api/?seed=1',
'https://randomuser.me/api/?seed=2',
'https://randomuser.me/api/?seed=3'
]

const promises = urls.map(url => fetch(url));

async function result() {
const responses = await Promise.all(promises);
return responses.map(async result => await result.json());
}

result().then(res => {
res.forEach(item => {
item.then(res => {
console.log(res.results)
})
})
});

awaitPromise.all 加入 try…catch 的 error handling

我們可以利用 try…catch 來承接 Promise.all 出現錯誤的狀況。
要注意的是, Promise.all 只要其中一個元素發生錯誤,其他結果會直接被忽略

範例如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const urls = [
'https://randomuser.me/api/?seed=1',
'https://randomuser.me/api/?seed=2',
'http://no-such-ulr' // 這個元素會發生錯誤
]

const promises = urls.map(url => fetch(url));

async function result() {
try {
const responses = await Promise.all(promises); // 會發生錯誤,並 throw error
return responses.map(async result => await result.json());
}
catch(err) {
alert(err);
return Promise.reject(new Error(err)) // 回傳出錯的內容
}
}

result().then(res => {
res.forEach(item => {
item.then(res => {
console.log(res.results)
})
})
})
.catch(alert) // 傳出來的錯誤在這裡被接起來

Homework

Task 1

改寫成 async…await 的用法

1
2
3
4
5
6
7
8
9
10
11
12
async function loadJson(url) {
const response = await fetch(url);
if(response.status === 200) {
return await response.json();
}

throw Error(response);
}

loadJson('https://randomuser.me/api/?seed=1')
.then(res => console.log(res.results[0].email))
.catch(alert)

Task 2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class HttpError extends Error {
constructor(response) {
super(`${response.status} for ${response.url}`);
this.name = name;
this.response = response;
}
}

async function loadJson(url) {
const response = await fetch(url);
if(response.status === 200) {
return response.json();
}

throw new HttpError(response);
}

async function demoUserInfo() {
const name = prompt('Enter a name?');
try {
const result = await loadJson(`https://no-such-url/?seed=${name}`);
console.log(result.results[0].name);
}
catch(err) {
alert(err)
}
}

demoUserInfo();

Task 3

1
2
3
4
5
6
7
8
9
10
11
async function wait() {
await new Promise(resolve => setTimeout(resolve, 1000));

return 10;
}

function f() {
wait().then(res => console.log(res));
}

f();

Reference

  1. https://javascript.info/async-await#await
  2. Example of top-level await in module