[JS] Introducing asynchronous JavaScript

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

Một vài note về lập trình bất đồng bộ trong JavaScript nào!

Async callbacks

Async callbacks là hàm cái mà được chỉ định như 1 đối số khi gọi 1 hàm, nó sẽ được thực thi bên trong. Sử dụng callbacks hơi lỗi thời nhưng bạn sẽ vẫn nhìn thấy chúng được sử dụng trong số lượng older-but-still-commonly-used APIs

Một ví dụ về async callback là tham số thứ 2 của phương thức addEventListener() như bên dưới:

1
2
3
4
5
6
7
btn.addEventListener('click', () => {
alert('You clicked me!');

let pElem = document.createElement('p');
pElem.textContent = 'This is a newly-added paragraph.';
document.body.appendChild(pElem);
});

Khi bạn truyền 1 callback function như 1 đối số trong hàm khác, bạn chỉ đang truyền tham chiếu của hàm đó, callback function này không thực thi ngay lập tức. Nó là “called back“ (do cái tên). Hàm chứa có trách nhiệm thực thi callback functions khi đến khi được gọi.

Chú ý rằng không phải tất cả các callback đều là bất đồng bộ, một vài cái là đồng bộ. Một ví dụ khi sử dụng Array.prototype.forEach() để lặp qua các items của 1 mảng, nó sẽ được thực thi ngay lập tức.

1
2
3
4
5
const gods = ['Apollo', 'Artemis', 'Ares', 'Zeus'];

gods.forEach(function (eachName, index){
console.log(index + '. ' + eachName);
});

Promises

Promises là new style of async code bạn sẽ nhìn thấy được sử dụng trong Web APIs hiện đại. Một ví dụ tốt nhất là fetch() API trong XMLHttpRequest():

1
2
3
4
5
6
7
8
fetch('products.json').then(function(response) {
return response.json();
}).then(function(json) {
products = json;
initialize();
}).catch(function(err) {
console.log('Fetch problem: ' + err.message);
});

Promise là 1 object đại diện cho sự thành công hoặc thất bại của toán tử bất đồng bộ. Nó đại diện cho 1 trạng thái trung gian. Thực chất trình duyệt muốn nói là “Tôi hứa sẽ trở về cho bạn với câu trả lời sớm nhất có thể“ giống như cái tên “promise”

Note: You’ll learn a lot more about promises later on in the module, so don’t worry if you don’t understand them fully yet.

The event queue

Các toán tử bất đồng bộ như promises sẽ được đặt vào hàng đợi, cái mà chạy sau khi luồng chính đã kết thúc để chúng không chặn các đoạn mã JavaScript đang chạy
Toán tử hàng đợi sẽ hoàn thành sớm nhất có thể sau đó trả về kết quả đến JavaScript environment.

Promises versus callbacks

Promises tương tự như old-style callbacks. Chúng thực chất là một đối tượng được trả về mà bạn đính kèm các callback functions, thay vì phải truyền vào trong hàm như callbacks.

Tuy nhiên Promises được đặt biệt sinh ra để xử lý các toán tử bất đồng bộ và có 1 vài ưu điểm hơn so với old-style callbacks:

  • Bạn có thể xử lý chuỗi nhiều toán tử bất đồng bộ với nhau bằng cách sử dụng nhiều toán tử .then(), truyền vào kết quả từng cái như 1 input. Điều này rất khó làm với callbacks, cái mà thường xuyên kết thúc với một “pyramid of doom” (hay còn được biết đến như là callback hell)
  • Promise callbacks luôn được gọi theo thứ tự nghiêm ngặt chúng được đặt trong hàng đợi sự kiện.
  • Xử lý lỗi tốt hơn, tất cả các lỗi được xử lý trong khối .catch(), khối cuối thay vì xử lý riêng lẻ trong mỗi level của “kim tự tháp”.
  • Promises sẽ tránh đảo ngược điều khiển, không giống như các old-style callback, mất toàn quyền kiểm soát cách thức thực hiện chức năng khi chuyển một callback đến thư viện của bên thứ ba. (kiểu nhận về kết quả promises vẫn kiểm soát được còn callback thì không)

Graceful asynchronous programming with Promises

The trouble with callbacks

Để hiểu tất cả tại sao Promises là tốt, chúng ta hãy xem Callbacks có vấn đề gì?
Hãy nói về việc đặt pizza:

  • Bạn chọn topping mà bạn muốn, nó có thể mất 1 thời gian nếu bạn thiếu quyết đoán và có thể thất bại nếu bạn không thể quyết định hoặc quyết định lấy một món cà ri thay thế.
  • Sau đó bạn đặt hàng. Nó cũng có thể mất 1 khoảng thời gian để trả về 1 cái pizza và có thể lỗi nếu nhà hàng không có nguyên liệu để nấu nó.
  • Sau đó bạn nhận pizza và ăn. Nó cũng có thể lỗi nếu bạn quên ví, vì vậy bạn không thể trả tiền cho chiếc pizza!

Với old-style callbacks, mã giả các hàm sẽ như sau:

1
2
3
4
5
6
7
chooseToppings(function(toppings) {
placeOrder(toppings, function(order) {
collectOrder(order, function(pizza) {
eatPizza(pizza);
}, failureCallback);
}, failureCallback);
}, failureCallback);

Điều này gây ra sự lộn xộn và khó đọc (thường được gọi là callback hell), các failureCallback() được gọi nhiều lần (mỗi lần trong hàm lồng) với các vấn đề khác bên cạnh.

Improvements with promises

Promises giúp giải quyết vấn đề như đoạn code bên dưới, dễ đọc, cấu trúc và chạy. Nếu chúng ta viết mã giả với promises nó sẽ như sau:

1
2
3
4
5
6
7
8
9
10
11
chooseToppings()
.then(function(toppings) {
return placeOrder(toppings);
})
.then(function(order) {
return collectOrder(order);
})
.then(function(pizza) {
eatPizza(pizza);
})
.catch(failureCallback);

Nhìn có vẻ tốt hơn rồi, thật dễ dàng để xem việc gì xảy ra tiếp theo và bạn chỉ cần 1 khối .catch() để xử lý tất cả các lỗi, nó không có block của luồng chính (bởi vậy bạn có thể chơi game trong khi chúng ta đang chờ pizza giao đến) và mỗi toán tử đảm bảo chờ các toán từ trước đó thành công trước khi chạy với khối .then().

Sử dụng arrow functions, code thậm chí còn đơn giản hơn:

1
2
3
4
5
6
7
8
9
10
11
chooseToppings()
.then(toppings =>
placeOrder(toppings)
)
.then(order =>
collectOrder(order)
)
.then(pizza =>
eatPizza(pizza)
)
.catch(failureCallback);

Thậm chí còn có thể:
1
2
3
4
5
chooseToppings()
.then(toppings => placeOrder(toppings))
.then(order => collectOrder(order))
.then(pizza => eatPizza(pizza))
.catch(failureCallback);

Thậm chí bạn có thể viết như sau, vì các hàm chỉ truyền trực tiếp đối số của chúng, do đó không cần thêm tầng của hàm:
1
chooseToppings().then(placeOrder).then(collectOrder).then(eatPizza).catch(failureCallback);

Tuy nhiên, điều này không dễ đọc và cú pháp này có thể không sử dụng được nếu các khối của bạn phức tạp hơn những gì chúng tôi đã trình bày ở đây.

Note: You can make further improvements with async/await syntax, which we’ll dig into in the next article.

Về cơ bản nhất, các Promises tương tự như event listeners, nhưng với một vài điểm khác biệt:

  • Một Promise chỉ có thể thành công hoặc thất bại một lần. Nó không thể thành công hoặc thất bại hai lần và nó không thể chuyển từ thành công sang thất bại hoặc ngược lại sau khi hoạt động đã hoàn thành.
  • If a promise has succeeded or failed and you later add a success/failure callback, the correct callback will be called, even though the event took place earlier.

Tổng kết:

  • Cả callback và Promise đều là code bất đồng bộ, không thực thi ngay lập tức (đa số callback). Promises có nhiều ưu điểm hơn callback như xử lý nhiều chuỗi toán tử với khối .then()
  • Bạn có nhớ: old-style callback, callback hell, “pyramid”
  • Promise được đặt ở đâu và xử lý khi nào? Câu trả lời là 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é).

    Async operations like promises are put into an event queue, which runs after the main thread has finished processing so that they do not block subsequent JavaScript code from running. The queued operations will complete as soon as possible then return their results to the JavaScript environment.

Tài liệu tham khảo

Introducing asynchronous JavaScript

Author

Ming

Posted on

2020-01-24

Updated on

2021-04-10

Licensed under

Comments