+76

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ái setTimeout 0Promise 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 🤣:

Screenshot 2024-07-07 at 6.21.30 PM.png

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ặc UI 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ỗi Page 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ủa setTimeout
  • 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é:

Callstack.jpg

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, đẩy console.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:

Callstack (1).jpg

Sau đó Event loop thực hiện lấy task ở Microtask queue đưa vào Callstack và thực hiện:

Callstack (2).jpg

Và cuối cùng là Macrotask queue:

Callstack (3).jpg

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ủa MutationObserver
  • Macrotask: setTimeout/setInterval, load <script ..., xử lý callback event onscroll/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:

Callstack (4).jpg

  • 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 ra begins
  • đẩ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 trong new code của ta cũng chạy đồng bộ như bên ngoài vậy, tức là cái console.log("promise 2") sẽ được đưa vào CallStack và thực hiện in ra promise 2 luôn, sau đó là tới setTimeout 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ử:

Callstack (5).jpg

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:

Callstack (6).jpg

  • 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)Promise.resolve()
  • Đẩy callback .then của cái Promise.resolve() vào Microtask queue

Đến giờ callstack lại trống:

Callstack (7).jpg

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:

Callstack (8).jpg

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:

Callstack (9).jpg 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:

Callstack (10).jpg 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:

Callstack (13).jpg

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ó ở trong function async1
  • tiếp theo thực hiện async2() (được gọi ở trong function 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ủa async1() sẽ được đưa vào microtask queue, tức là cái console.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 trong new 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:

Callstack (14).jpg

Thứ tự thực hiện thì vẫn như thường lệ, Microtask queue trước:

Callstack (15).jpg

Sau khi Microtask queue xong, Callstack trống thì job trong Macrotask queue mới được chạy:

Callstack (16).jpg

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:

Callstack (18).jpg

Ở đâ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ọn n=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:

Screenshot 2024-07-13 at 4.25.27 PM.png

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:

Screenshot 2024-07-13 at 4.29.58 PM.png

Ầ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:

Screenshot 2024-07-13 at 4.48.54 PM.png

Để 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:

ezgif-5-36e0799308.gif

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:

Callstack (18).jpg

Ở đâ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à:

  1. 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
  2. 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: Callstack (18).jpg

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

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí