最常見的事件循環 (Event Loop) 面試題目彙整

2023年1月26日

💎 加入 E+ 成長計畫 與超過 250+ 位軟體工程師一起在社群中成長,並且獲得更深入、系統性軟體工程內容

事件循環 (Event Loop) 是面試的常考題,在《請說明瀏覽器中的事件循環 (Event Loop)》一文當中,我們統整了事件循環的基礎概念。然而,在實際面試中,除了概念外,很常會透過程式碼來考察對於事件循環的理解。這邊我們根據不同的難度,整理了常見的程式碼判讀題,以及每題的解說。

基礎題

console.log(1);

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

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

看完上面的程式碼,你覺得會印出什麼呢? 答案在下方,我們一起來分析。

setTimeout 設定 0 毫秒,這樣為什麼會是 Promise 裡面的東西先執行呢? 原因是 Promise 會進到微任務列隊,而 setTimeout 會是在宏任務列隊。在一次事件循環中,宏任務一次只提取一個,所以 console.log(1) 後,會先去看微任務列隊,不斷提取到執行棧中直到微任務列隊為空,因此這邊會先執行 Promise ,然後才是setTimeout

1;
3;
4;
2;

中階題

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);
});

在讀完上面的程式碼,你覺得實際執行後,會印出什麼呢? 答案如下,讓我們一起來分析。

  1. 程式碼執行後會依順序執行程式,所以這時會先印出 'begins'
  2. 接著遇到 setTimeout 會把它放到宏任務列隊;然後遇到 new Promise 會先執行,印出 'promise 2' ,然後又遇到一個 setTimeout 所以把它放到宏任務列隊。
  3. 接著主線程又空了,所以去檢查宏任務列隊,執行列隊中的最先的那個 setTimeout,這時印出 'setTimeout 1' ,然後遇到 Promise.resolve() ,把它放到微任務列隊。
  4. 因為宏任務每次只會執行第一個項目,所以這時會去看微任務列隊,發現裡面有第三步放入的 Promise.resolve() 所以印出 'promise 1'
  5. 這時微任務列隊空了,所以回去看宏任務列隊,裡面有個第二步放的 setTimeout ,所以印出 'setTimeout 2'
  6. 然後因為這邊呼叫了 resolve 所以進入到 .then 於是印出 'dot then 1'
  7. 以及再把 setTimeout 放到宏任務列隊,因為這時微任務列隊已經是空的,所以把宏任務列隊中的 setTimeout 放到執行棧,然後執行 console.log(res),因為剛剛第六步 resolve 的值是 resolve 1 ,所以最後印出 resolve 1
"begins";
"promise 2";
"setTimeout 1";
"promise 1";
"setTimeout 2";
"dot then 1";
"resolve 1";

進階題

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");

這一題大家覺得會印出什麼呢? 跟上一題的差別是這題有 async 的語法。答案在下方,我們一樣一行行分析。

  1. 程式碼執行後會依順序執行程式,所以這時會先印出 'script start',接著把setTimeout 把它放到宏任務列隊
  2. 然後呼叫 async1 函式,印出 'async1 start'
  3. 然後呼叫 await async2() 所以印出 'async2'。注意, await 後的程式碼會被放到微任務列隊,所以不會馬上印出 'async1 end' 而是會把它放到微任務列隊
  4. 接著程式繼續執行,遇到 new Promise 先印出裡面的 'promise 1'
  5. 然後呼叫 resolve ,把 .then 的放到微任務列隊。程式繼續執行,印出 'script end'
  6. 這時候執行棧空了,所以去檢查微任務列隊,先印出第三步放的 'async1 end'
  7. 因為微任務列隊會一路執行到沒東西,所以繼續看微任務列隊,發現裡面還有剛剛第四步驟放入的 resolve 程式碼,所以印出 'promise2'
  8. 這時微任務列隊空了,去看宏任務列隊,有第一步放入宏任務列隊的 setTimeout 所以把它印出
"script start";
"async1 start";
"async2";
"promise1";
"script end";
"async1 end";
"promise2";
"setTimeout";

小結

希望透過以上三個題目,大家對於事件循環的判讀題有更高的掌握!

🧵 如果你想收到最即時的內容更新,可以在 FacebookInstagram 上追蹤我們