Event loop trong Javascript, microtask, macrotask, Promise và các câu hỏi phỏng vấn phổ biến
Hello các bạn lại là mình đây 👋👋
Dạo này các bạn thế nào? Công việc và cuộc sống có suôn sẻ không? Hôm nay chúng ta sẽ dành thời gian để cùng mình tìm hiểu một chủ đề rất thú vị trong JavaScript: Event Loop, microtask, macrotask, Promise và một số câu hỏi phổ biến liên quan trong phỏng vấn Software Engineer nhé (nhất là các vị trí frontend) 🥰
Chủ đề ngày hôm nay nghe có vẻ "hack não" nhưng mình tin chắc chắn là khi các bạn nắm được cách mà những đoạn code hàng ngày ta vẫn viết hoạt động như thế nào thì các bạn sẽ lên trình đáng kể đó 😎😎.
Thực tế chuyên môn của mình là Frontend, và có rất nhiều thứ mình muốn chia sẻ chút kiến thức nhỏ mọn của mình với mọi người, nhưng mà mỗi lần lên Viblo viết bài thì toàn viết về Docker/Kubernetes các thứ 😂
Lên thuyền với mình đê 🚢🚢
Điều mình hay thắc mắc
Lấy ví dụ đoạn code sau:
console.log('Start');
setTimeout(() => {
console.log('Timeout');
}, 0);
Promise.resolve().then(() => {
console.log('Promise');
});
console.log('End');
Dành 1 phút để thành thật với lòng mình, liệu các bạn có rõ chính xác thứ tự thực thi của các đoạn console.log
bên trên như thế nào không? Chính xác nhé 😆
Khi gặp đoạn code trên, mình đã từng có những suy nghĩ như thế này:
- à ừm thì
setTimeout 0
thì chắc là thực hiện ngay - Promise mà resolve ngay thì chắc cũng thế
- 2 cái
console.log
phải chạy trước, còn 2 cáisetTimeout 0
vàPromise resolve
thì chắc là tuỳ hoàn cảnh tuỳ vào JS nó chạy cái nào, không đoán trước 😂😂
Một ví dụ thực tế hơn nữa là với React, khi ta muốn truy cập vào DOM sau khi react đã re-render component thì ta thường dùng useEffect
, nhưng các bạn có để ý là ta cũng có thể dùng setTimeout 0
:
import { useEffect, useState } from "react";
function App() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log("[useEffect] text:", document.getElementById("text").textContent);
}, [count]);
return (
<div className="Main">
<div>
<button
onClick={() => {
setCount((c) => c + 1);
console.log("text:", document.getElementById("text").textContent);
setTimeout(() => {
console.log("text:", document.getElementById("text").textContent);
});
}}
>
Increase Count:<span id="text"> {count}</span>
</button>
</div>
</div>
);
}
Liệu ta có bao giờ thắc mắc 🤔🤔
Hay anh em nào code Angular mà gặp lỗi ExpressionChangedAfterItHasBeenCheckedError
thì lại wrap trong setTimeout 0
mà chưa chắc hiểu lí do vì sao nó lại fix được 🤣:
Một ví dụ nữa như sau:
let i = 0;
let start = Date.now();
function count() {
// do a heavy job
for (let j = 0; j < 1e9; j++) {
i++;
}
alert("Done in " + (Date.now() - start) + 'ms');
}
count();
Ở trên ta cần thực hiện 1 job nặng (heavy), demo của mình là for loop 1 tỉ lần (1e9
) sau đó alert
ra thời gian chạy.
Nếu các bạn chạy đoạn code này lên ta có thể thấy rõ là trong khi code nó chạy thì toàn bộ trình duyệt sẽ bị treo. Các bạn có thể test trực tiếp tại console của trình duyệt luôn (F12 Inspect)
1e9 nhanh quá chưa cảm nhận thì các bạn thử 1e10 xem nhé 🤣
Việc trình duyệt bị treo cho ta thấy rõ ràng là Main thread (UI thread) đang bị block. Và tất nhiên là khi làm thật ta nên cố gắng làm sao tránh trường hợp này...
Thử tượng tượng ta gặp một trong những câu hỏi bên trên khi phỏng vấn, có thể là ở dạng lí thuyết hoặc được yêu cầu fix bug... liệu ta có thể sống sót ? 🤪
Trong bài hôm nay ta sẽ cùng nhau tìm hiểu cặn kẽ cội nguồn từ đó trang bị súng đạn đầy đủ lần tới đi interview gặp những câu tương tự thì mạnh mẽ chém gió nhé.
Let's gooooooooooo 🚀🚀🚀
Event Loop là gì?
Ban đầu mình tính sẽ tự lên idea để giải thích về Event Loop trong JS, nhưng viết đi viết lại vẫn thấy rối rắm😅. May quá nhớ tới 1 clip Youtube này mình mới xem được gần đây
Phải nói là sau nhiều năm được xem nhiều clip giải thích về Event loop thì đây có lẽ là một trong những clip mình thấy ấn tượng nhất, Wow luôn, đỉnh thặc sự, vì cách mà người ta diễn đạt một khái niệm phức tạp một cách cực kì trực quan dễ hiểu 💯
Các bạn dành 10' để xem trước khi mình tiếp tục nhé, cực kì quan trọng đó nha:
Âu kê, sau khi các bạn đã xem hết thì ta tổng hợp lại chút nhé:
- Javascript phía browser (và Nodejs) đều được thực thi dựa trên một thứ gọi là Event loop. Đây là vũ khí "bí mật" giải thích lí do vì sao Javascript là single-thread nhưng lại có thể chạy những tác vụ async (bất đồng bộ) ví dụ như
setTimeout/setInterval, fetch
,... như thể ta đang có nhiều thread (như các ngôn ngữ khác vậy) - Về cơ bản các bạn hiểu đơn giản cơ chế hoạt động là có 1 cái
loop
chạy liên tục, và thực hiện mọi thứ có trong Call stack - Task Queue (hay còn gọi là
Macrotask queue
) là nơi chứa callback của Macrotask: ví dụsetTimeout, setInterval,...
Microtask queue
sẽ chứa những callback của Microtask: ví dụ Promise, MutationObserver- Macrotask sẽ chỉ được thực hiện sau khi Callstack đã trống và Microtask queue trống
- 1 Microtask có thể trigger 1 microtask khác, và dẫn tới microtask queue không bao giờ trống và cứ thế chạy mãi có thể gây giật lag hoặc thậm chí đơ trình duyêt. Nhưng với Macrotask thì không thế, vì mỗi lần Macrotask thực hiện xong nó sẽ trả control về cho Event loop. Hay nói cách khác thì mỗi macrotask được thực hiện trên 1 iteration của Event loop (dạng như 1 lần lặp)
- Ta có thể trực tiếp đẩy 1 callback vào Microtask queue dùng
queueMicrotask
, ví dụ:
queueMicrotask(() => {
console.log(1)
})
Bên cạnh đó cũng có thêm nhưng điều ta cần biết mà video không đề cập tới:
- với Worker thì nó sẽ được chạy trên 1 thread khác, và có event loop của riêng nó. Thường khi ta nói
Main Thread
(hoặcUI Thread
) ý chỉ tới cái thread chính dùng để làm việc với UI, hầu hết là code của ta được thực thi ở thread này,Main thread
mà bị block thì web của chúng ta sẽ bị treo và Chrome sẽ báo lỗiPage unresponsive
- chú ý rằng với mỗi thread, ta chỉ có 1 Event loop: ví dụ Main thread có 1 event loop, mỗi Web worker có 1 event loop riêng. Và khi ta mở mỗi tab trình duyệt thì sẽ là 1 môi trường riêng biệt có Event loop riêng
- Tất cả các Microtasks phải được thực hiện xong trước khi trình duyệt xử lý event handling/rendering hoặc trước khi thực hiện Microtask tiếp theo
Chốt lại, về cơ bản ta có thứ tự thực hiện như sau: Code đồng bộ > Microtask > Macrotask
Các câu hỏi hay gặp trong phỏng vấn
Ta sẽ cùng đi qua các câu hỏi phổ biến mình tổng hợp được để ta có thể hiểu hơn về cách Event loop hoạt động nhé
Tiểu học vỡ lòng
Ta cùng xem đoạn code sau:
console.log(1);
setTimeout(function () {
console.log(2);
}, 0);
Promise.resolve()
.then(function () {
console.log(3);
})
.then(function () {
console.log(4);
});
Áp dụng đúng lý thuyết ở trên ta có:
console.log(1)
: code đồng bộ (synchronous code)console.log(2)
: Macrotask vì nó là call back củasetTimeout
console.log(3)
và sau đó làconsole.log(4)
: là Microtask vì là callback của Promise
Vậy thứ tự thực hiện ở đây sẽ là:
console.log(1)
console.log(3)
console.log(4)
console.log(2)
Ta đi cụ thể hơn để xem Call Stack ở đây có những gì nhé:
Event loop sẽ thực hiện theo thứ tự như sau:
- đưa
console.log(1)
vào CallStack --> lấy ra và thực hiện luôn, in ra console là1
- tiếp tục đẩy
setTimeout
vào Callstack -> lấy ra và thực hiện, đẩyconsole.log(2)
vào Macrotask queue - đưa
Promise.resolve
vào Callstack-> vẫn tiếp tục lấy từ stack ra và thực hiện, đẩy 2 cái console.log ở trong 2 cái.then
của nó lần lượt vào Microtask queue
Chú ý Call stack như trong hình thực hiện từ dưới lên, queue thực hiện từ trái qua phải
Đến bước này thì Callstack đang trống:
Sau đó Event loop thực hiện lấy task ở Microtask queue đưa vào Callstack và thực hiện:
Và cuối cùng là Macrotask queue:
Và khi mọi thứ trống trải thì ta cũng có nói là "Event loop hoàn thành 1 lần lặp/duyệt" (hết 1 "iteration" hoặc hết 1 "tick")
Ủa vậy có những cái nào là Macro cái nào là Microtask ý nhờ ?? 🤔🤔
Cụ thể như sau:
- Microtask: callback của Promise bao gồm
then/catch/finally
, và callback củaMutationObserver
- Macrotask:
setTimeout/setInterval
, load<script ...
, xử lý callback eventonscroll/onclick...
và rất nhiều thứ khác
Hi vọng là các bạn đã thấm được ví dụ đầu tiên này 😁
Nâng cao hơn chút
Trong khi interview thì khả năng ta ít khi gặp câu vỡ lòng như trên, mà sẽ gặp những dạng tầm như sau:
console.log("begins");
setTimeout(() => {
console.log("setTimeout 1");
Promise.resolve().then(() => {
console.log("promise 1");
});
}, 0);
new Promise(function (resolve, reject) {
console.log("promise 2");
setTimeout(function () {
console.log("setTimeout 2");
resolve("resolve 1");
}, 0);
}).then((res) => {
console.log("dot then 1");
setTimeout(() => {
console.log(res);
}, 0);
});
Nếu bị hỏi những câu như này trong phỏng vấn thì các bạn cứ từ từ bình tĩnh, nhớ kĩ công thức thứ tự ưu tiên: Code đồng bộ > Microtask (Promise then/catch/finally) > Macrotask (setTimeout/setInterval)
, đảm bảo qua môn 🤣
Ta cùng phân tích câu trên nhé 🚀
Khi chạy đoạn code trên, ở lần chạy đầu tiên của Event Loop (first iteration) thì ta có như sau:
- Vẫn như thường lệ, đẩy
console.log("begins")
vào Callstack -> sau đó lấy ra luôn và thực hiện, in rabegins
- đẩy cái setTimeout đầu tiên ngay sau
begins
vào Callstack -> đưa callback của setTimeout vào Macrotask queue - Tiếp tục tới cái
new Promise
, với Javascript thì bên trongnew
code của ta cũng chạy đồng bộ như bên ngoài vậy, tức là cáiconsole.log("promise 2")
sẽ được đưa vào CallStack và thực hiện in rapromise 2
luôn, sau đó là tớisetTimeout
và đẩy callback của nó vào Macrotask queue
Chú ý là ở hình vẽ bên trên, Callstack mình để nhiều thứ vào trong đó cùng lúc, chứ thực tế là cứ đẩy vào là chúng được lấy ra luôn để thực thi
Và hiện tại Callstack của ta trống, trong Macrotask queue có 2 phần tử:
Và vẫn như thường lệ, ta tiếp tục với thứ tự thực hiện từ trái qua phải:
- Lấy phần từ đầu tiên từ Macrotask queue ra và thực hiện thì ta có cái
console.log(setTimeout 1)
vàPromise.resolve()
- Đẩy callback
.then
của cáiPromise.resolve()
vào Microtask queue
Đến giờ callstack lại trống:
Trước khi thực hiện tiếp thì ta lặp lại câu thần chú một lần nữa: Code đồng bộ > Microtask (Promise then/catch/finally) > Macrotask (setTimeout/setInterval)
🙏🙏
Tức là ở đây tiếp theo Event loop sẽ lấy task từ Microtask queue ra và thực hiện:
Sau đó Callstack trống, Microtask queue cũng trống nên Event loop sẽ lấy 1 item còn lại trong Macrotask queue ra và thực hiện:
Tiếp tục, console.log(2)
được thực hiện, sau đó tới resolve
, và khi resolve thì Event loop đẩy cái callback của Promise (.then
) vào trong Microtask queue:
Sau đó lại lấy ra đem vào Callstack và thực hiện, thì ta có console.log(dot then 1)
, cái setTimeout thì callback của nó lại đẩy vào Macrotask queue và vì hiện tại Microtask trống nên sau đó đem ra thực hiện luôn và cuối cùng ta có resolve 1
được in ra (phần này mình đi nhanh chút các bạn tự thẩm xem nhé 😎)
Tổng hợp, kết quả khi chạy đoạn code này là:
"begins";
"promise 2";
"setTimeout 1";
"promise 1";
"setTimeout 2";
"dot then 1";
"resolve 1";
Đấy các bạn ạ, 1 bài như này thẩm đã khó, vấn đề là làm sao để khi interview mình có thể giải thích trôi chảy để người phỏng vấn người ta hiểu và thấy thuyết phục.
Cũng toát mồ hôi đấy 😂😂
Nâng cao thêm chút nữa
Ôn ngon nghẻ lắm rồi, nhưng đến khi zô interview lại gặp quả xoáy hơn như thế này:
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}
async function async2() {
console.log("async2");
}
console.log("script start");
setTimeout(function () {
console.log("setTimeout");
}, 0);
async1();
new Promise(function (resolve) {
console.log("promise1");
resolve();
}).then(function () {
console.log("promise2");
});
console.log("script end");
Lại đọc câu thần chú Code đồng bộ > Microtask (Promise then/catch/finally) > Macrotask (setTimeout/setInterval)
...🙏
Ủa chớt rồi, thần chú không có await
😳😳. Thank you and see you again luôn 🤣🤣
Ở đây ta phải học thêm 1 thần chú nữa đó là: những thứ sau await
sẽ bị đưa vào microtask
queue, cụ thể ở đây là cái console.log("async1 end");
ở function async1
Khi đã có được thần chú mới rồi thì cách làm về cơ bản là vẫn giống như phần trước, khi chạy đoạn code trên ta có thứ tự thực hiện như sau:
Giải thích:
- đầu tiên
console.log(script start)
được đưa vào callstack và lấy ra thực hiện luôn, kết quả được in ra console - tiếp đó là setTimeout vào Callstack, khi đó thì cái callback của nó được đẩy vào Macrotask queue
- tiếp theo là function
async1()
vào call stack - thực hiện
console.log(async1 start)
có ở trongfunction async1
- tiếp theo thực hiện
async2()
(được gọi ở trongfunction async1
) - tiếp theo
console.log(async2)
được thực hiện - Và bởi vì ta đang
await async2()
nên như mình đã nói: phần sau đó củaasync1()
sẽ được đưa vào microtask queue, tức là cáiconsole.log("async1 end")
(cái màu tím 🟣 trong ảnh) - Tiếp theo tới
new Promise > console.log(promise1)
là code đồng bộ nên được thực thi ngay - tiếp đó là cái
resolve()
bên trongnew Promise
được gọi, callback.then
của nó được đưa vào Microtask queue
Sau khi đã thực hiện tất cả bước trên thì Callstack đã trống và chỉ còn Microtask + Macrotask queue là còn job:
Thứ tự thực hiện thì vẫn như thường lệ, Microtask queue trước:
Sau khi Microtask queue xong, Callstack trống thì job trong Macrotask queue mới được chạy:
Và cuối cùng kết quả ta có sẽ là:
"script start";
"async1 start";
"async2";
"promise1";
"script end";
"async1 end";
"promise2";
"setTimeout";
Cũng chuối đó nhờ, cái này mà đầu nhảy số không nhanh trong lúc interview là chân đi lạnh toát à 🤣🤣
Note cực quan trọng: khi bạn await
1 cái Promise, thì Event loop sẽ chờ cho cái Promise đó resolve
thì mới chạy tiếp. Ta xem ví dụ sau:
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}
async function async2() {
console.log("async2 start");
await new Promise((resolve) => {
console.log("async2 promise");
setTimeout(() => {
console.log("async2 setTimeout");
resolve();
}, 0);
});
console.log("async2 end");
}
async1();
Ở trên cái console.log("async2 end")
sẽ chỉ được thực hiện khi mà cái resolve()
được chạy vì ta có await new Promise
.
Với điều kiện như vậy thì các bạn thử dự đoán kết quả in ra khi chạy đoạn code trên sẽ là gì 😉😉
Cải thiện rendering performance
P1. Chia nhỏ task ngốn CPU
Ví dụ khi phỏng vấn ta có thể gặp một câu hỏi như sau: cho đoạn code bên dưới, hiện tại khi bấm Start thì cả trình duyệt bị treo. Yêu cầu tối ưu hiệu năng render, không block trình duyệt
<!DOCTYPE html>
<h1 id="count">
Count: 0
</h1>
<button onclick="start()">
Start
</button>
<div id="log">
Log:
</div>
<script>
"use strict";
let count = 0
setInterval(() => {
document.getElementById('count').innerHTML = `Count: ${++count}`
}, 1000)
function start() {
let i = 0;
let start = Date.now();
function count() {
// do a heavy job
for (let j = 0; j < 2e9; j++) {
i++;
}
document.getElementById('log').innerHTML = "Log: Done in " + (Date.now() - start) + 'ms'
}
count();
}
</script>
Ở đây ta thực hành trực tiếp với JSFiddle nhé:
chú ý khi thực hành, có thể mỗi lần chạy hơi lâu chút (vài chục giây), nhưng các bạn hạn chế scroll đi chỗ khác, hoặc đổi tab nhé, vì với các tab inactive thì setTimeout sẽ cho kết quả hơi khác chút, cái này phần FAQ mình sẽ giải thích nhé (mỗi khi bấm Start ta ngồi chờ kết quả trả về đã nha)
Khi chạy đoạn code này lên, bấm nút Start
, ngay lập tức ta thấy trình duyệt JSFiddle bị treo, Counter dừng hoạt động. Cho tới khi for
loop chạy xong trả về log thì trình duyệt mới responsive trở lại và counter tiếp tục đếm
Ta bắt đầu phân tích và xử bài này nhé 💪💪
Đầu tiên ta có thể nhanh chóng phát hiện ra nguyên nhân chính dẫn tới trình duyệt bị treo là do ta có cái for
loop chạy đồng bộ (synchronous), và nó chạy tới 2 tỉ lần (2e9
), dẫn tới Event loop nó sẽ chạy hết cái task đó trước khi thực hiện các task khác (ví dụ: rendering).
Ta xem ảnh sau:
Ở đây ta có thêm một bước nữa trong câu thần chú: đó là Event loop sẽ chạy code đồng bộ trước sau đó tới Microtask và khi nó thực hiện xong Microtask thì mới tới Rendering, và sau đó là Macrotask
Và ở ví dụ của bài này thì Code đồng bộ (for
loop), nó chạy lâu quá, dẫn tới phần Rendering bị blocked
Và từ đây ý tưởng của ta là sẽ chia nhỏ cái for loop ra và chạy thành nhiều lần. Cụ thể:
- Tách ra thành các "đợt" lặp nhỏ, mỗi "đợt" nhỏ này ta cho lặp
n
lần, ta cần phải chọn sốn
sao cho hợp lý nếu không là nó lại block Event loop đó nha. Ở đây ta chọnn=1 triệu
(1e6) là có vẻ hợp lý. - Ở mỗi "đợt" chạy, ta vẫn làm như bình thường, và khi chạy xong thì ta cho trình duyệt chút thời gian, trước khi chạy "đợt" mới. Và để làm điều này thì ta sẽ dùng
setTimeout
, bởi vì Rendering chạy trước Macrotask mà. Nên cứ chạy xong 1 đợt -> nghỉ, dành thời gian cho Rendering -> chạy tiếp đợt mới
Ban đầu lặp 1 phát 2 tỉ lần, thì giờ chia thành 2 tỉ / 1 triệu = 2 nghìn đợt, mỗi đợt 1 triệu lần
Với ý tưởng như vậy thì ta sửa lại code một chút như sau nhé:
Ở đây ta đổi từ for
sang dùng do/while
, các bạn thích dùng for
haywhile
cũng được nha 😁
Ta cứ do/while bao giờ đủ 1 triệu (1e6
), tức là hết 1 "đợt", thì ta sẽ check xem đã chạy đủ 2 tỉ lần chưa, nếu đủ thì update Log và thoát, nếu chưa đủ thì gọi đệ quy (recursive) thực hiện "đợt" tiếp theo bằng setTimeout
. Với cách này mỗi khi hết 1 "đợt" ta đang cho Event loop chút thời gian để nó làm công việc khác (Rendering)
Và giờ ta bấm Start
sẽ thấy rằng trình duyệt vẫn hoạt động như bình thường, không có hiện tượng giật lag treo máy nữa, Counter chạy bình thường trong lúc lặp 💪💪
Như ở đây của mình chạy hết 24 giây là xong:
P1.0 đánh giá hiệu suất
Sau khi ta đã đưa ra được giải pháp như trên, thì interviewer có thể hỏi follow up thêm là, liệu giải pháp đã thực sự tối ưu chưa, ta có thể làm nó tốt hơn được không?
Đây có thể là 1 câu follow up, hoặc người ta cho mình đáp án ngay từ đầu và yêu cầu tối ưu
Giờ ta thử đưa setTimeout
từ sau do/while lên trước khi do/while xem nhé:
Sau đó ta chạy lại:
Ầu, xuống còn 17 giây 🧐🧐
Lí do ở đây là gì nhờ? Thứ tự trước sau mà ảnh hưởng vậy hử? 😳 Tưởng là callback của setTimeout
thì kiểu gì cũng bị thực hiện sau code đồng bộ chứ?
Thực tế là có khác nhau đó các bạn 😂. Ta đọc thêm ở đây nhé: https://developer.mozilla.org/en-US/docs/Web/API/setTimeout#passing_string_literals (mục "Nested timeouts")
Tức là nếu ta gọi setTimeout liên tục, lồng nhau, và đã lồng tới 5 lần, thì từ lần thứ 6 trở đi, trình duyệt sẽ cho 1 chút delay bằng 4ms
. Tức là khi ta gọi setTimeout
thì browser sẽ delay chừng 4ms
mới bắt đầu thực hiện.
Do vậy ta gọi setTimeout
càng sớm thì tổng thể ví dụ bài này sẽ chạy nhanh hơn
P2. Hiển thị tiến độ - progress
Cho đoạn code sau:
Đề bài: hiện tại mỗi khi bấm Start thì trình duyệt bị treo, yêu cầu là hiển thị số lần đã lặp tăng dần, không block UI
Ta để ý rằng, với code hiện tại, mỗi khi ta bấm Start thì counter cũng đứng hình luôn, sau khi vòng for
hoàn thành thì hiển thị ra số lần đã lặp và counter mới chạy tiếp:
Để UI không bị block, vẫn responsive, thì ý tưởng vẫn như phần trên, tách nhỏ vòng lặp lớn thành nhiều "đợt" lặp nhỏ, hết mỗi đợt thì ghi kết quả ra UI, để trình duyệt thực hiện Rendering, sau đó chạy tiếp:
Ở trên ta vẫn dùng do/while
, mỗi "đợt" cho chạy 1000 lần (1e3
), nếu chạy chưa đủ 5 triệu (5e6
) thì gọi setTimeout
để chạy "đợt" mới
Kết quả khi chạy lên nom sẽ như sau:
Như các bạn thấy thì giờ tiến độ đã được cập nhật ngay lập tức, mượt hơn rất nhiều 🚀🚀
FAQ
Ta cùng xem qua một số câu hỏi thường gặp nhé
Gọi lồng nhau
Ta xem lại sơ đồ sau:
Ở đây ta chú ý rằng, trước khi thực hiện Rendering hoặc chạy 1 Macrotask, thì Eventloop sẽ phải chạy hết code đồng bộ, sau đó chạy hết job ở trong Microtask queue, tức là nếu ta liên tục đẩy job vào Microtask queue thì trình duyệt cũng có thể bị treo luôn
Để đẩy job vào Microtask queue thì ta có thể dùng Promise
hoặc queueMicrotask()
Giờ ta sửa lại kết quả phần hiển thị tiến độ bên trên 1 chút, dùng queueMicrotask
thay vì setTimeout
nhé:
Giờ khi ta bấm Start thì trình duyệt lại bị treo như thường, vì ta đang đẩy job vào Microtask queue liên tục, và Event loop phải chạy hết job trong Microtask queue trước khi thực hiện Rendering
Timeout với tab inactive
Với những tab nào mà inactive, tức là người dùng không focus vào tab đó, thì trình duyệt sẽ áp dụng 1 chút delay, xêm xêm cái 4ms
delay ta nói ở trên với Nested setTimeout.
Xem thêm ở đây: https://developer.mozilla.org/en-US/docs/Web/API/setTimeout#passing_string_literals (mục "Timeouts in inactive tabs")
setTimeout với delay > 0
Ví dụ đoạn code sau:
setTimeout(() => {
console.log('Hello, World!')
}, 3000)
Câu hỏi là:
- cái callback của setTimeout có được đưa vào Macrotask queue ngay lập tức, và chờ 3 giây thì mới thực hiện
- hay là nó sẽ chờ 3 giây rồi mới đưa vào Macrotask queue?
Đáp án 2 là đáp án đúng: sau 3 giây thì callback mới được đưa vào Macrotask queue. Đoạn xử lý delay là do trình duyệt sẽ có bộ đến timer và khi hết thời gian nó sẽ đẩy callback vào Macrotask queue. Chú ý rằng đây là nhiệm vụ của trình duyệt (hoặc NodeJS runtime) chứ không phải của Eventloop
Bài về nhà
Cho các ví dụ sau, yêu cầu đưa ra đáp án thứ tự thực hiện và giải thích
Ví dụ 1
console.log("script start");
setTimeout(function() {
console.log("setTimeout 1 start");
setTimeout(function() {
console.log("nested setTimeout 1");
new Promise(function(resolve) {
console.log("promise in nested setTimeout 1");
resolve();
}).then(function() {
console.log("then in nested setTimeout 1");
});
}, 0);
new Promise(function(resolve) {
console.log("promise in setTimeout 1");
resolve();
}).then(function() {
console.log("then in setTimeout 1");
setTimeout(function() {
console.log("nested setTimeout 2");
new Promise(function(resolve) {
console.log("promise in nested setTimeout 2");
resolve();
}).then(function() {
console.log("then in nested setTimeout 2");
});
}, 0);
});
console.log("setTimeout 1 end");
}, 0);
new Promise(function(resolve) {
console.log("outer promise");
resolve();
}).then(function() {
console.log("outer promise then");
return new Promise(function(resolve) {
console.log("nested promise");
resolve();
}).then(function() {
console.log("nested promise then");
});
});
setTimeout(function() {
console.log("setTimeout 2 start");
new Promise(function(resolve) {
console.log("promise in setTimeout 2");
resolve();
}).then(function() {
console.log("then in setTimeout 2");
});
console.log("setTimeout 2 end");
}, 0);
console.log("script end");
Ví dụ 2
console.log("script start");
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}
async function async2() {
console.log("async2 start");
await new Promise(resolve => {
console.log("async2 promise");
setTimeout(() => {
console.log("async2 setTimeout");
resolve();
}, 0);
});
console.log("async2 end");
}
setTimeout(() => {
console.log("setTimeout 1 start");
setTimeout(() => {
console.log("nested setTimeout 1");
new Promise(resolve => {
console.log("promise in nested setTimeout 1");
resolve();
}).then(() => {
console.log("then in nested setTimeout 1");
});
}, 0);
new Promise(resolve => {
console.log("promise in setTimeout 1");
resolve();
}).then(() => {
console.log("then in setTimeout 1");
setTimeout(() => {
console.log("nested setTimeout 2");
new Promise(resolve => {
console.log("promise in nested setTimeout 2");
resolve();
}).then(() => {
console.log("then in nested setTimeout 2");
});
}, 0);
});
console.log("setTimeout 1 end");
}, 0);
async1();
new Promise(resolve => {
console.log("outer promise");
resolve();
}).then(() => {
console.log("outer promise then");
return new Promise(resolve => {
console.log("nested promise");
resolve();
}).then(() => {
console.log("nested promise then");
});
});
setTimeout(() => {
console.log("setTimeout 2 start");
new Promise(resolve => {
console.log("promise in setTimeout 2");
resolve();
}).then(() => {
console.log("then in setTimeout 2");
});
console.log("setTimeout 2 end");
}, 0);
console.log("script end");
Đừng quá sợ vì code dài, hãy nhớ mấy câu thần chú mình nói trong bài 😎
Recap
Phewwww bài cũng dài phết, viết xong tí tụt áp suất 🤣🤣
Ta cùng recap lại chút nhé:
- Trình duyệt (và phía NodeJS server) chúng đều có 1 thực thể là Event loop để thực thi code của chúng ta
- Về cơ bản Event loop nó là 1 cái vòng lặp vô hạn, cứ thấy có Job là chạy
- câu thần chú:
code đồng bộ > Microtask > Macrotask
và nếu cóawait
thì tất cả những gì sau await sẽ được đưa vào Microtask queue - Macrotask chỉ được chạy nếu Callstack + Microtask queue trống
- Chú ý khi nào trình duyệt thực hiện Rendering:
Qua bài này hi vọng mình đã trang bị thêm cho các bạn ít súng đạn để có thể xử lý một số câu hỏi phổ biến trong phỏng vấn Software Engineering
Chúc các bạn cuối tuần vui vẻ, hẹn gặp lại các bạn vào những bài sau 👋
All rights reserved