Microtasks Trong JavaScript

JavaScript là một ngôn ngữ chạy đơn luồng (single thread), nghĩa là trong một chương trình chỉ tồn tại một call stack duy nhất. Với những tác vụ nặng và tốn nhiều thời gian, toàn bộ chương trình tại thời điểm đó sẽ bị blocking cho đến khi tác vụ đó hoàn thành. Lúc này chúng ta không thể thao tác được bất cứ thứ gì trên trình duyệt.

Để giải quyết vấn đề này, các JavaScript Engine đã cung cấp các API để có thể chạy các đoạn code JavaScript bất đồng bộ. Và để có thể chạy bất đồng bộ, JavaScript sử dụng thêm một thành phần nữa gọi là Microtasks.

Microtasks là gì?

Microtasks là một cấu trúc dữ liệu dạng FIFO (First In - First Out). Nó thường lưu những tác vụ được thực hiện bởi Web APIs như setTimeout, DOM events, hay là những tác vụ xử lý liên quan đến Promise hoặc khi sử dụng hàm queueMicrotask.

Microtasks queue

Các tác vụ không bất bộ cần được quản lý thích hợp. Vì vậy, tiêu chuẩn ECMA chỉ định một hàng đợi nội bộ PromiseJobs, thường được gọi là “microtask queue”.

Đặc điểm:

  • Hàng đợi thực thi theo cơ chế First In - First Out tức là vào trước và ra trước.
  • Việc thực thi một tác vụ chỉ được bắt đầu khi không có gì khác đang chạy.

Hay nói một cách đơn giản hơn, khi một promise đã sẵn sàng, các trình xử lý .then/catch/finally sẽ được đưa vào hàng đợi; chúng vẫn chưa được thực hiện. Khi JavaScript "Rảnh", nó sẽ nhận một tác vụ từ hàng đợi và thực thi nó.

Promise handlers luôn đi qua hàng đợi này. Nếu có một chuỗi với nhiều .then/catch/finally, thì mọi chuỗi trong số chúng được thực thi bất đồng bộ. Có nghĩa là, đầu tiên nó được xếp vào hàng đợi, sau đó được thực thi khi code hiện tại hoàn tất và các trình xử lý được xếp hàng trước đó đã hoàn thành.

Unhandled rejection

Bây giờ chúng ta có thể thấy chính xác cách JavaScript phát hiện ra unhandled rejection. "Unhandled rejection" xảy ra khi một promise bị lỗi và nó không được xử lý ở cuối hàng đợi microtask". Thông thường, nếu chúng ta có một lỗi , chúng ta sẽ thêm .catch vào chuỗi promise để xử lý nó:

let promise = Promise.reject(new Error("Promise Failed!"));
promise.catch(err => alert('caught'));

// doesn't run: error handled
window.addEventListener('unhandledrejection', event => alert(event.reason));

Nhưng nếu chúng ta quên thêm .catch, thì sau khi hàng đợi microtask trống, công cụ sẽ kích hoạt sự kiện:

let promise = Promise.reject(new Error("Promise Failed!"));

// Promise Failed!
window.addEventListener('unhandledrejection', event => alert(event.reason));

Nếu sau này chúng ta xử lý lỗi thì sao? Nó sẽ như thế này:

let promise = Promise.reject(new Error("Promise Failed!"));
setTimeout(() => promise.catch(err => alert('caught')), 1000);

// Error: Promise Failed!
window.addEventListener('unhandledrejection', event => alert(event.reason));

Bây giờ, nếu chúng ta chạy nó, chúng ta cầm xem xét Promise Failed! trước và sau đó caught.

Nếu chúng ta không biết về microtasks queue, chúng ta có thể tự hỏi: “Tại sao unhandledrejection lại chạy được? ”. Nhưng bây giờ chúng ta hiểu rằng unhandledrejection được tạo ra khi microtasks queue hoàn tất: công cụ kiểm tra các promise và nếu bất kỳ promise nào trong số chúng ở trạng thái “rejected”, thì sự kiện sẽ kích hoạt. Trong ví dụ trên, .catch được thêm bởi các setTimeout. Nhưng nó làm như vậy sau đó, sau khi unhandledrejection đã xảy ra, vì vậy nó không thay đổi bất cứ điều gì.

Tổng kết

Promise handling luôn bất đồng bộ, vì tất cả các promise đều đi qua hàng đợi “promise jobs”, còn được gọi là “microtasks queue ”. Vì vậy, các trình xử lý .then/catch/finally luôn được gọi sau khi code hiện tại kết thúc. Nên chúng ta cần đảm bảo rằng một đoạn mã được thực thi sau đó  .then/catch/finally, cần thêm vào nó một chained.then. Vậy là chúng nhau đã đi hết chặng đường tìm hiểu về Microtasks trong bài học này rồi! Hẹn gặp lại các bạn ở bài học sau async, await nhé!

Trần Quang Hào
SUNTECH VIỆT NAM   Đăng ký để nhận thông báo mới nhất