+4

Event loop và callback queue trong JavaScript

JavaScript là ngôn ngữ lập trình đơn luồng (một câu lệnh (dòng code) được thực thi tại một thời điểm) và có mô hình thực thi đồng bộ (mỗi câu lệnh sẽ được thực thi theo thứ tự xuất hiện).

Vậy phải thế nào khi ta cần đợi một khoảng thời gian trước khi có thể thực thi một số đoạn code? Có thể là ta phải chờ dữ liệu từ API/server request, hay cần đợi cho timer hoàn thành và thực thi code.

Trong core JavaScript engine, chúng ta có 3 phần chính:

  • Thread of execution
  • Memory/variable environment
  • Call stack

Giờ đã có thêm 1 số thành phần mới:

Ở bài viết này chúng ta sẽ tìm hiểu cách event loopcallback queue hoạt động.

Dẫn nhập

Ta có chương trình như sau:

Khi ta chạy chương trình, nó sẽ thực thi, lần lượt như sau:

1. Khai báo function printHello ở global memory.

2. Dòng 5 - Thực thi setTimeout(printHello,1000). Lúc này nhiệm vụ được chuyển sang Web Browser Feature: Timer.

Nhưng ta đã gửi gắm gì cho Timer? Là duration: 1000ms kèm theo printHello

Lúc này ta chuyển đến Web Browser Features và set một Timer cho việc này. Và vì lúc này là 1ms, status của Timer chưa hoàn thành, nên ta sẽ trở lại global.

3. Trở lại với global, thread of execution đi tới và thực thi dòng 7 - in ra Me first! - ở thời điểm 2ms

Lưu ý: Khoảng thời gian qua, bộ đếm thời gian của Timer vẫn đang tíc tắc tíc tắc nhé.

4. Khi thời gian từ lúc bắt đầu chạy chương trình đạt 1000ms, trạng thái của Timer được xác nhận hoàn thành và dựa trên On Completion, function printHello sẽ được đẩy vào Call stack và hàm printHello sẽ được thực thi, in ra Hello

Event Loop & Callbacks Queue

JavaScript engine ưu tiên thế nào khi thực thi code?

  • Code ở global sẽ được thực thi trước, theo thứ tự xuất hiện.
  • Sau đó nó kiểm tra xem call stack có đang trống không? Nếu call stack trống, nó sẽ lấy code ra từ callback queue - nơi chúng ta thêm vào callbacks function.

Nghĩa là nếu global đã thực thi xong và call stack trống, JS engine sẽ đi đến callbacks queue để lấy callback function và đẩy vào call stack để thực thi.

Nhưng ai - cái gì, đi kiểm tra những việc ở trên? Event loop!

Event loop, là tính năng mà:

  • Đi kiểm tra xem global còn code không? Nếu còn thì thực thi nó.
  • Kiểm tra xem call stack có gì không? Nếu có thì thực thi nó.
  • Nếu tất cả đã thực thi xong, chuyển tới callback queue và bắt đầu từ cái cũ nhất - lấy nó và thêm vào call stack để thực thi.

Quay trở lại với chương trình ở trên

1ms printHello sẽ được đẩy vào callback queue, sau đó chương trình thực thi bình thường. Khi thời gian từ lúc bắt đầu chạy chương trình đạt 1000ms:

  • printHello: Ê tao hết thời gian bị hoãn rồi, cho tao trở lại đi.
  • Event loop: Uh rồi, biết mày được phép trở lại rồi, để tao coi coi tới lượt mày chưa.

Sau đó event loop làm nhiệm vụ của nó là kiểm tra global execution context đã thực thi tất cả code ở global chưa, và xem call stack có đang trống hay không. Nếu các điều kiện trên thoả mãn thì nó sẽ đi đến callback queue và lấy printHello ra và đẩy vào call stack.

Nhưng nếu ở ví dụ trên, tại thời điểm 1000ms mà 2 điều kiện trên chưa thoả mãn thì sao? Nếu bạn chưa chắc chắn thì đừng lo, ta sẽ hoàn toàn hiểu khi xem ví dụ tiếp theo.

Vì sau những bài viết về JavaScript chúng ta đã quen thuộc với thread of execution, execution context, call stack nên mình sẽ đi nhanh hơn mà không đi từng bước cách chương trình được JS engine thực thi nữa. Nếu bạn biết đến bài này trước khi đọc những bài đầu thì hãy dành thời gian để đọc nhé.

1. Tại thời điểm 1ms dòng 9 được thực thi - setTimeout(printHello,0).

Ta đã gửi gắm gì cho Timer? Là duration: 0ms kèm theo printHello.

Lúc này ta chuyển đến Web Browser Features và set một Timer cho việc này, Lúc này là 1ms, duration là 0ms, status của Timer đã hoàn thành, vậy ta có đưa printHello về call stack và thực thi nó luôn không? Không!

Hãy nhớ lại ở phần event loop, ta có nói chỉ khi thoả các điều kiện sau thì mới tới lượt code trong callback queue được chạy:

  • Global execution context đã thực thi xong hết code ở global
  • Call stack đang trống

Ngay lúc này ở global vẫn còn code chưa được thực thi, nên event loop sẽ chưa xem tới callback queue. Toàn cảnh lúc này đang là:

2. Với 1 trong 2 điều kiện chưa thoả mãn, ta quay lại global - lúc này đang là 2ms, thực thi dòng 11 đẩy blockFor1Sec() vào call stack. Function này làm gì kệ nó, cứ giả định là nó sẽ tốn của chúng ta 1s hay 1000ms.

3. Sau khi thực thi xong, JS engine lại trở về global và tiếp tục thực thi. Lúc này đang là 1002ms, call stack đã trống. Nhưng ở global vẫn còn code để thực thi!

Đến dòng 13 và thực thi, in ra Me first!

4. Lúc này, code ở global đã thực thi xong hết, call stack cũng trống, đã thoả điều kiện để event loop đi gặp callback queue và kiểm tra xem callback queue có cần gì không. Và vì có printHello đã đợi từ lâu, nó được đẩy vào call stack và thực thi, in ra Hello

Lúc này chương trình đã thực thi xong nên global execution context sẽ bị xoá.

Kết bài

Vậy là ta đã tìm hiểu về event loop, và callback queue hoạt động như thế nào, ở bài sau ta sẽ tìm hiểu về micro task queue và Promise. (Microtask queue - call back queue?? cái nào sẽ được ưu tiên hơn? Đọc bài về Promise để biết nhé!)

Bài này nằm trong chuỗi bài viết về JavaScript của em/ mình khi đang học, nếu có hiểu sai hay còn thiếu xót mong các bạn, anh chị góp ý!


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.