setTimeout(fn, 0) // it works - JavaScript 事件迴圈 動畫演示

AaronLin發表於2024-04-17

在前端程式碼中很經常看到使用 setTimeout(fn, 0),如下面程式碼所示,乍一看很多餘,但是移除了可能會出現一些奇奇怪怪的問題。要解釋這個就需要理解 事件迴圈(Event Loop),下面會透過一些例子和動畫來輔助理解事件迴圈

setTimeout(() => {
  // 呼叫一些方法
}, 0)

為什麼使用事件迴圈

JS 是單執行緒的(瀏覽器和 Node則是多執行緒的),為了避免 渲染主執行緒 阻塞,需要非同步,事件迴圈 是非同步的實現方式

瀏覽器在一個渲染主執行緒中執行一個頁面中的所有 JavaScript 指令碼,以及呈現佈局,迴流,和垃圾回收。為了避免 同步 的執行方式導致渲染主執行緒阻塞,使得頁面卡死,所以瀏覽器採用非同步的方式:渲染主執行緒將任務交給其他執行緒去處理,自身 立即結束 任務的執行,轉而執行後續程式碼,當其他執行緒完成時,將事先傳遞的回撥函式包裝成任務,加入到對應的訊息佇列的末尾排隊,等待渲染主執行緒排程執行

流程:

  1. 渲染主執行緒執行全域性 JS,需要非同步的任務放到對應的佇列,如果是 setTimeout 則會有執行緒計時,到了指定時間會將任務放入 延時佇列(並非立即執行)
  2. 渲染主執行緒為空時,按佇列的優先順序依次選擇佇列(最先執行微佇列的任務),依次按順序執行各個佇列的任務

任務沒有優先順序,而訊息佇列有優先順序,不同任務分屬於不同佇列:參考 W3C 規範微佇列優先順序最高,接著是互動佇列然後才是延時佇列

常見佇列:

  • 微佇列(microtask):⽤戶存放需要最快執⾏的任務,優先順序「最⾼」,透過 Promise.resolve().then() ⽴即把⼀個函式新增到微佇列
  • 互動佇列:⽤於存放⽤戶操作後產⽣的事件處理任務,優先順序「⾼」
  • 延時佇列:⽤於存放計時器到達後的回撥任務,優先順序「中」

事件迴圈

下面例子來自於:《WEB前端大師課》,大塊的文字描述相對沒那麼直觀,所以用 Keynote 做了 gif 方便理解(如果有更好的做 gif 的方式可以留言告訴我)

1. JS阻礙頁面渲染

JS 修改了 DOM 後,並不會馬上顯示在頁面上,需要進行 繪製 後才會顯示頁面變更

<!DOCTYPE html>
<html lang="en">
  <head></head>
  <body>
    <h1>初始h1</h1>
    <button>change</button>
    <script>
      var h1 = document.querySelector('h1');
      var btn = document.querySelector('button');

      function delay(duration) {
        var start = Date.now();
        while (Date.now() - start < duration) {}
      }
      
      btn.onclick = function () {
        h1.textContent = '修改h1 textContent';
        delay(3000);
      };
    </script>
  </body>
</html>

01繪製任務

效果:點選 change 後,頁面卡死,3s 後 h1 內容變更為:修改h1 textContent

2. 延遲佇列

setTimeout 到達指定時間可能並不會立即執行

setTimeout(function () {
  console.log(1);
}, 0);

function delay(duration) {
  var start = Date.now();
  while (Date.now() - start < duration) {}
}

delay(3000);

console.log(2);

02settimeout

效果:卡死 3s 後輸出 2 1

3. 微佇列

使用 Promise.resolve().then 可以將任務直接新增到微佇列

setTimeout(function () {
  console.log(1);
}, 0);

Promise.resolve().then(function () {
  console.log(2);
});

console.log(3);

03微佇列

效果:依次輸出 3 2 1

4. 複雜情況

function a() {
  console.log(1);
  Promise.resolve().then(function () {
    console.log(2);
  });
}
setTimeout(function () {
  console.log(3);
  Promise.resolve().then(a);
}, 0);

Promise.resolve().then(function () {
  console.log(4);
});

console.log(5);

04複雜Promise

效果:依次輸出 5 4 3 1 2

擴充

理解了上面的概念,可以嘗試分析一下 現代 JavaScript 教程 事件迴圈例子,檢查一下是否理解了事件迴圈

參考資料

2024 年我還在寫這樣的程式碼
為什麼 JS 要加入 setTimeout, css 的 transition 才能生效
深入理解和使用 Javascript 中的 setTimeout(fn,0)
主執行緒
併發模型與事件迴圈
非同步 JavaScript
排程:setTimeout 和 setInterval

相關文章