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.
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!
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 cái gì đi kiểm tra những thứ 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 đó nó 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 kia thoả mãn thì nó sẽ đi đến callback queue và lấy printHello
ở đó 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? Ta sẽ hoàn toàn hiểu khi xem ví dụ tiếp theo.
Do sau 3 bài viết 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 thực thi nữa.
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 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
và ở dòng 11 đẩy blockFor1Sec()
vào call stack để thực thi. 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. Lúc này đang là 1002ms
, blockFor1Sec
đã thực thi xong, 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 xem có gì 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.
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 ý!
Em cũng đang tìm việc về Python (Django), JavaScript fresher/intern ạ. Mong có cơ hội làm việc với mọi người.