一、概念

JavaScript 是單線程執行(基於執行棧 / 調用棧 call stack),事件循環負責不斷地從各種任務隊列裏取任務執行——以保證異步任務的函數回調按規則有序運行,瀏覽器環境和 Node.js 環境都使用事件循環,儘管他們的事件循環邏輯並不相同。

之所以函數的執行基於“棧”這種結構,是因為 js 函數允許嵌套,先調用的函數需要等待內部函數的調用執行完畢才能執行,也就是先調用後執行的邏輯,正好滿足“棧”這種數據結構。

執行棧/調用棧是針對函數調用來説的,而我們 js 任務的執行依賴於任務隊列,先進入隊列的任務會先執行,而且一個任務中可能存在多個函數。要注意一個是函數調用的機制,一個是任務執行的機制,不是一回事!

二、基本構件

  • ​**Call Stack(調用棧)**​:同步代碼入棧執行、執行完出棧。
  • 宏任務(macrotask / task)隊列​:例如 setTimeoutsetIntervalsetImmediate(Node)、DOM 事件、I/O 回調、UI 渲染觸發等。

    宏任務作為之前的一種籠統叫法,現代瀏覽器對這些任務做了更細的劃分,對他們都統稱為了 task,不同的任務具有不同的隊列。不過,微任務的概念一直被保留使用。

  • 微任務(microtask / job)隊列​:例如 Promise.then/catch/finallyasync+awaitqueueMicrotaskMutationObserver(瀏覽器)、process.nextTick(Node)。
  • ​**渲染/繪製階段(browser)**​:在合適時機把更新繪製到屏幕(通常在 macrotask 完成並且 microtasks 已清空之後)。

    主要為了後面對於任務執行和瀏覽器渲染順序的理解。

  • ​**事件循環(event loop)**​:不斷循環——執行一個 macrotask → 清空所有 microtasks → 執行渲染(若需要) → 下一個 macrotask。

三、瀏覽器裏的執行模型

循環的每一輪(tick)大致順序:

  1. macrotask queue 取出一個任務並執行(例如頁面初始 script)。
  2. 當前任務執行完後,立即運行並清空 ​microtask queue​(每出現一個 microtask,它會被加入隊列;直到隊列空才返回)。

    microtasks 在同一輪裏可能不斷產生並被立即處理。

  3. 當 microtasks 清空後,會進行一次 ​渲染/繪製​(如果需要)。
  4. 進入下一輪 macrotask。

結論:​microtask 的優先級高於下一個 macrotask​。

這裏多提一嘴“tick”,不知道有多少同學看到這個“tick”,馬上就會聯想到 Vue 中的 nextTick,其實,他們確實有一定淵源。

事件循環中的 tick :

tick = 一次事件循環的執行週期 = Task → Microtask → Render → 下一 tick

而 Vue.nextTick 作用試講 DOM 更新後的回調放入微任務隊列(或者退化為宏任務),主要是為了解決 DOM 的異步更新導致無法得到最新 DOM。Vue 源碼邏輯:

if (Promise) microtask
else if (MutationObserver) microtask // 舊瀏覽器
else macrotask fallback // setImmediate(IE專屬) -> setTimeout(Macrotask,最差)

Vue 官方文檔對於 nextTick 的解釋是:等待下一次 DOM 更新刷新的工具方法。和事件循環中的 tick 何其相似。

除此之外,對於瀏覽器渲染和事件循環結合很多同學沒有了解過,以下是一個結合瀏覽器渲染的例子:

<script>
console.log('start');

setTimeout(() => console.log('timeout'), 0);

Promise.resolve().then(() => console.log('promise'));

requestAnimationFrame(() => console.log('raf'));

console.log('end');
</script>

在瀏覽器輸出:start end promise raf timeout

解釋:

  • 同步:startend
  • microtask: promise
  • 渲染相關:requestAnimationFrame 在下一幀渲染前執行(在 microtasks 清空後,但通常在 macrotask 之前的 render 時機),所以 raftimeout 之前
  • setTimeout 是下一輪 macrotask,所以最後輸出。

四、Node.js(libuv)與瀏覽器的區別

Node 的底層是 libuv,事件循環分多個階段:

  1. timers(處理 setTimeout/setInterval)
  2. pending callbacks(I/O 回調)
  3. idle, prepare(內部使用)
  4. poll(檢索新的 I/O 事件並執行)
  5. check(處理 setImmediate)
  6. close callbacks(socket close 等)

微任務(Promise callbacks)是在每個階段執行後 ​立即清空​(microtask checkpoint);另外 Node 有 process.nextTick,其優先級甚至高於 microtasks(會在當前階段馬上執行,且會在 Promise microtasks 之前運行)。

我不想放很多面試題去講解,因為面試題是做不完的,而知識的核心重點就是這些。

上面的理論搞懂了,基本上相關面試題都可以做對。