[JS] Making asynchronous programming easier with async and await

Thái Bình, chiều 30 Tết, trời âm u, se se lạnh …

Async/await mới được thêm gần đây, 1 phần của so-callled ECMAScript 2017 JavaScript edition. Chức năng này cải thiện cú pháp của Promises, làm cho code bất đồng bộ dễ dàng viết và đọc hơn. Nó làm cho async code nhìn giống old-school code đồng bộ do đó chúng rất đáng để học. Bài viết này chỉ cho bạn cái bạn cần biết.

The basics of async/await

Có 2 phần để sử dụng async/await trong code của bạn

The async keyword

Cái đầu tiên chúng ta có là từ khóa async, cái mà đặt trước khai báo hàm để tạo một hàm bất đồng bộ. Thử làm quen nào :

1
2
function hello() { return "Hello" };
hello();

Gọi hàm bây giờ sẽ trả về 1 promise. Đây là 1 trong đặc điểm của async functions - nó biến bất kỳ một hàm nào thành 1 promise.

Bạn cũng có thể viết async function expression như sau:

1
2
let hello = async function() { return "Hello" };
hello();

Hay arrow functions:
1
let hello = async () => { return "Hello" };

Đó là tất cả những thứ đơn giản giống nhau.

Để thực sự lấy giá trị trả về của 1 promise hoàn thành, vì nó trả về một promise, chúng ta có thể sử dụng khối .then()

1
hello().then((value) => console.log(value))

Hay thậm chị có thể viết ngắn hơn:
1
hello().then(console.log)

Túm cái váy lại, từ khóa async được thêm vào các hàm để nói rằng chúng trả về 1 promise thay vì trả về trực tiếp kết quả. Thêm vào đó, nó cũng cho phép các hàm đồng bộ tránh được các chi phí phát sinh khi chạy với sự hỗ trợ khi sử dụng await. Bạn chỉ cần thêm từ khóa async khi khai báo, JavaScript có thể tối ưu hóa chương trình của bạn cho bạn. Sweet!

The await keyword

Ưu điểm thực sự của async function trở nên rõ ràng khi bạn kết hợp nó với từ khóa await. Nó có thể được đặt trước 1 vài async promise-based function để dừng code của bạn tại dòng đó cho tới khi promise hoàn tất, sau đó trả về giá trị kết quả. Trong khi đó, các mã khác vẫn được thực thi.

Bạn cũng có thể sử dụng await khi gọi 1 vài hàm cái mà trả về Promise trong các web API functions.

1
2
3
4
5
async function hello() {
return greeting = await Promise.resolve("Hello");
};

hello().then(alert);

Tất nhiên ví dụ trên là vô nghĩa, nó chỉ dùng làm ví dụ minh họa về cú pháp.

Rewriting promise code with async/await

Hãy nhìn lại ví dụ từ bài trước về fetch(), chúng ta đã viết theo async functions:

1
2
3
4
5
6
7
8
9
10
11
fetch('coffee.jpg')
.then(response => response.blob())
.then(myBlob => {
let objectURL = URL.createObjectURL(myBlob);
let image = document.createElement('img');
image.src = objectURL;
document.body.appendChild(image);
})
.catch(e => {
console.log('There has been a problem with your fetch operation: ' + e.message);
});

Nhưng bây giờ bạn nên có 1 thành 1 chuyên gia xử lý promise và có thể áp dụng async/await, viết lại nhìn đơn giản hơn biết bao nhiêu:
1
2
3
4
5
6
7
8
9
10
11
async function myFetch() {
let response = await fetch('coffee.jpg');
let myBlob = await response.blob();

let objectURL = URL.createObjectURL(myBlob);
let image = document.createElement('img');
image.src = objectURL;
document.body.appendChild(image);
}

myFetch();

Đoạn code đã trở nên đơn giản hơn và dễ dàng để hiểu - không có khối .then() nào nữa.

Từ khi từ khóa async biến 1 hàm trở thành promise, bạn có thể refactor đoạn code của bạn sử dụng kết hợp promise và await, đưa nửa sau đoạn code của bạn vào trong khối mới linh hoạt hơn.

1
2
3
4
5
6
7
8
9
10
11
async function myFetch() {
let response = await fetch('coffee.jpg');
return await response.blob();
}

myFetch().then((blob) => {
let objectURL = URL.createObjectURL(blob);
let image = document.createElement('img');
image.src = objectURL;
document.body.appendChild(image);
});

But how does it work?

Bạn được chú ý rằng chúng ta sẽ nhóm đoạn code bên trong hàm và chúng sẽ khai báo async trước từ khóa function. Điều này thực sự cần thiết, bạn phải tạo ra 1 hàm async cái mà sẽ chạy code bất đồng bộ, await chỉ làm việc trong hàm async functions.

Trong hàm myFetch(), bạn có thể thấy đoạn code khá giống với phiên bản promise trước đó, nhưng có 1 số khác biệt. Thay vì cần xâu chuỗi khối .then() cuối mỗi phương thức promise, bạn chỉ cần thêm 1 từ khóa await trước phương thức được gọi và sau đó gán kết quả vào 1 biến. Từ khóa await sẽ làm đoạn code của bạn dừng lại ở dòng đó, cho phép các đoạn code khác thực thi trong lúc đó, cho đến khi async function đã trả về kết quả của nó. Một khi điều này hoàn tất, code của bạn tiếp túc thực thi dùng tiếp theo. Ví dụ

1
let response = await fetch('coffee.jpg');

Response sẽ được trả về khi hoàn tất fetch() promise và được gán cho biến response khi đã phản hồi và trình phân tích cú pháp tạm dừng trên dòng này cho đến khi điều đó xảy ra. Một khi đã phản hồi, trình phân tích sẽ di chuyển tới dòng tiếp theo, cái mà sẽ tạo Blob. Dòng này cũng gọi đến async promise-based method, vì chúng ta sử dụng await ở đây là đúng. Khi kết quả của toán tử trả về, chúng ta sẽ trả nó ra ngoài hàm myFetch().

Điều đó có nghĩa là khi chúng ta gọi hàm myFetch(), nó sẽ trả về 1 promise, do đó chúng ta cần 1 chuỗi .then() sau đó để hiện thị blod ra màn hình.

Bạn có thể đã nghĩ “this is really cool!” và bạn đã đúng - ít khối .then() hơn bọc code của bạn và nó nhìn cũng như code đồng bộ và trực quan hơn.

Adding error handling

Nếu bạn muốn thêm 1 xử lý lỗi, sẽ có 1 vài tùy chọn cho bạn.

Bạn có thể sử dụng khối try/catch với async/await

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
async function myFetch() {
try {
let response = await fetch('coffee.jpg');
let myBlob = await response.blob();

let objectURL = URL.createObjectURL(myBlob);
let image = document.createElement('img');
image.src = objectURL;
document.body.appendChild(image);
} catch(e) {
console.log(e);
}
}

myFetch();

Khối catch truyền vào 1 error object, cái mà chúng ta gọi là e, hiện tại chỉ hiển thị ra, bạn có thể xử lý khác

Nếu bạn muốn sử dụng cách thứ 2 (đã refactor), bạn có thể sử dụng khối .catch() cuối cùng khối .then()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
async function myFetch() {
let response = await fetch('coffee.jpg');
return await response.blob();
}

myFetch().then((blob) => {
let objectURL = URL.createObjectURL(blob);
let image = document.createElement('img');
image.src = objectURL;
document.body.appendChild(image);
})
.catch((e) =>
console.log(e)
);

Bạn có thể xem cả 2 ví dụ này tại
simple-fetch-async-await-try-catch.html (see source code) và
simple-fetch-async-await-promise-catch.html (see source code)

Awaiting a Promise.all()

async/await là built on top of promise, bởi vậy nó tương thích với tất cả các tính năng của Promises. Chức năng này bao gồm Promise.all() - bạn có thể khá hạnh phúc khi await 1 Promise.all() lấy tất cả kết quả được trả về vào 1 biến, code giống như đồng bộ

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
30
31
32
33
34
35
36
37
38
39
40
41
async function fetchAndDecode(url, type) {
let response = await fetch(url);

let content;

if(type === 'blob') {
content = await response.blob();
} else if(type === 'text') {
content = await response.text();
}

return content;
}

async function displayContent() {
let coffee = fetchAndDecode('coffee.jpg', 'blob');
let tea = fetchAndDecode('tea.jpg', 'blob');
let description = fetchAndDecode('description.txt', 'text');

let values = await Promise.all([coffee, tea, description]);

let objectURL1 = URL.createObjectURL(values[0]);
let objectURL2 = URL.createObjectURL(values[1]);
let descText = values[2];

let image1 = document.createElement('img');
let image2 = document.createElement('img');
image1.src = objectURL1;
image2.src = objectURL2;
document.body.appendChild(image1);
document.body.appendChild(image2);

let para = document.createElement('p');
para.textContent = descText;
document.body.appendChild(para);
}

displayContent()
.catch((e) =>
console.log(e)
);

Bạn có thể thấy rằng fetchAnDecode() function đã được chuyển thành async function và dòng
1
let values = await Promise.all([coffee, tea, description]);

Bằng cách sử dụng await, chúng ta có thể nhận tất cả kết quả trả về của 3 promises trong mảng values khi mà tất cả chúng đều có giá trị, cách này nhìn rất giống code đồng bộ. Chúng ta có thể bọc tất cả code trong bất đồng bộ displayContent() làm code trở nên dễ đọc hơn rất nhiều.

Để xử lý error, chúng ta có khối .catch() trong displayContent(), khối này sẽ xử lý các lỗi xuất hiện ở cả 2 functions

Note: Bạn cũng có thể sử dụng khối finally với async function to show a final report on how the operation went

The downsides of async/await: Nhược điểm của async/await

Async/await thực sự hữu ích như chúng ta đã biết, nhưng nó vẫn có 1 vài nhược điểm.

Async/await làm code của bạn nhìn như đồng bộ và trong 1 vài cách nó làm cho code đồng bộ hơn. Khối từ khóa await thực thi tất cả code đến khi promise hoàn tất chính xác như nó sẽ làm với một hoạt động đồng bộ. Nó cũng cho phép các tasks khác tiếp tục chạy trong lúc đó nhưng code của bạn thì bị chặn.

Điều đó có nghĩa code của bạn sẽ chậm hơn bởi 1 số lượng đáng kể các promise đang đợi ngay sau đó. Mỗi await sẽ chờ promise trước đó kết thúc, trong khi thực tế cái mà bạn muốn là các promises sẽ xử lý đồng thời.

Có một cách hữu hiệu có thể xử lý vấn đề này - đặt tất cả các quá trình xử promise trong một Promise objects, gán nó vào biến và sau đó await tất cả chúng sau.

Bạn có thể xem 2 ví dụ sau để thấy sự khác biệt, cùng phần tích nhé.

Cả 2 đều bắt đầu với 1 custom promise function cái mà xử lý bất đồng bộ với setTimeout():

1
2
3
4
5
6
7
function timeoutPromise(interval) {
return new Promise((resolve, reject) => {
setTimeout(function(){
resolve("done");
}, interval);
});
};

Sau đó hàm async timeTest() sẽ chờ 3 timeoutPromise():
1
2
3
async function timeTest() {
...
}

Sau mỗi lần sẽ đo thời gian thực thi của timeTest() promise và in kết quả ra:
1
2
3
4
5
6
let startTime = Date.now();
timeTest().then(() => {
let finishTime = Date.now();
let timeTaken = finishTime - startTime;
alert("Time taken in milliseconds: " + timeTaken);
})

Bây giờ chúng ta sẽ có timeTest() cho mỗi trường hợp.

Trong ví dụ slow-async-await.html, timeTest() nhìn như sau:

1
2
3
4
5
async function timeTest() {
await timeoutPromise(3000);
await timeoutPromise(3000);
await timeoutPromise(3000);
}

Đây là việc sử dụng await đơn giản cho 3 lời gọi timeoutPromise() trực tiếp, mối cái mất 3s, cái sau chờ cái trước chạy xong như vậy sẽ mất 9s.

Hơi tù nhỉ, nhưng trong fast-async-await.html, timeTest() sẽ như sau:

1
2
3
4
5
6
7
8
9
async function timeTest() {
const timeoutPromise1 = timeoutPromise(3000);
const timeoutPromise2 = timeoutPromise(3000);
const timeoutPromise3 = timeoutPromise(3000);

await timeoutPromise1;
await timeoutPromise2;
await timeoutPromise3;
}

Bây giờ chúng ta lưu 3 Promise objects vào biến, có tác dụng thiết lập các tiến trình liên quan của chúng, tất cả chạy đồng thời.

Tiếp theo chúng ta chờ đợi kết quả, bởi vì các promises bắt đầu quá trình thời gian giống nhau, tất cả sẽ kết thúc cùng 1 lúc và khi bạn chạy ví dụ thứ 2 này, bạn sẽ thấy chỉ mất 3s!

Hãy nhớ điều này nhé, vì nó ảnh hưởng đến hiệu suất đó :D

Còn 1 sự bất tiện nữa là bạn phải bọc await 1 promises bên trong một hàm async.

Async/await class methods

Phần chú ý cuối cùng trước khi chúng ta sang chủ đề mới là bạn có thể thêm async vào class/objetcs methods để làm cho chúng trả về promise sau đó await các promise bên trong chúng

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Person {
constructor(first, last, age, gender, interests) {
this.name = {
first,
last
};
this.age = age;
this.gender = gender;
this.interests = interests;
}

async greeting() {
return await Promise.resolve(`Hi! I'm ${this.name.first}`);
};

farewell() {
console.log(`${this.name.first} has left the building. Bye for now!`);
};
}

let han = new Person('Han', 'Solo', 25, 'male', ['Smuggling']);

Sau đó có thể sử dụng như sau:
1
han.greeting().then(console.log);

Browser support

Bạn hãy cân nhắc khi quyết định sử dụng async/await hỗ trợ các trình duyệt cũ. Chúng chỉ khả dụng với các phiên bản trình duyệt mới, như promise, có thể không hỗ trợ với Internet Explorer và Opera Mini.

Nếu bạn muốn sử dụng async/await trên các trình duyệt cũ, hãy xem xét sử dụng BabelJS library, cái mà cho phép bạn viết ứng dụng với kỹ thuật JavaScript mới nhất và để Babel tìm ra những thay đổi cần thiết của trình duyệt của bạn.

Khi gặp một trình duyệt không hỗ trợ async / await, polyfill của Babel có thể tự động cung cấp các dự phòng hoạt động trong các trình duyệt cũ hơn.

Tổng kết:

  • async/await là cú pháp refactor cho các Promise, rất hữu ích. Nó làm code cho bạn dễ đọc, như luồng đồng bộ, loại bỏ các khối .then().
  • Khi gặp await keywork, code của bạn sẽ dừng ở đó, các task khác (code ngoài khối asyn/await này) vẫn chạy. Khi nào hàm async đang await trả về kết quả, code bên trong khối async này sẽ chạy dòng tiếp theo.

The await keyword blocks execution of all the code that follows until the promise fulfills, exactly as it would with a synchronous operation. It does allow other tasks to continue to run in the meantime, but your own code is blocked.

The await keyword causes the JavaScript runtime to pause your code on this line, allowing other code to execute in the meantime, until the async function call has returned its result. Once that’s complete, your code continues to execute starting on the next line

  • Có một cách khắc phục nhược điểm chờ đợi của async rất hay đó là gán các async function vào các biến và chờ chúng sau đó.
  • Nhắc lại Promise chạy như thế này ở bài trước để hiểu về async/await chạy nhé, vì async/await thực chất là Promise mà: Promise được đặt trong hàng đợi sự kiện và xử lý khi luồng chính đã kết thúc để tránh block luồng chính (JS ngôn ngữ đơn luồng nhé).

Tài liệu tham khảo

Making asynchronous programming easier with async and await

[JS] Making asynchronous programming easier with async and await

http://yoursite.com/2020/01/24/JS-Making-asynchronous-programming-easier-with-async-and-await/

Author

Ming

Posted on

2020-01-24

Updated on

2021-04-10

Licensed under

Comments