一、概念
JavaScript 是單線程執行(基於執行棧 / 調用棧 call stack),事件循環負責不斷地從各種任務隊列裏取任務執行——以保證異步任務的函數回調按規則有序運行,瀏覽器環境和 Node.js 環境都使用事件循環,儘管他們的事件循環邏輯並不相同。
之所以函數的執行基於“棧”這種結構,是因為 js 函數允許嵌套,先調用的函數需要等待內部函數的調用執行完畢才能執行,也就是先調用後執行的邏輯,正好滿足“棧”這種數據結構。
執行棧/調用棧是針對函數調用來説的,而我們 js 任務的執行依賴於任務隊列,先進入隊列的任務會先執行,而且一個任務中可能存在多個函數。要注意一個是函數調用的機制,一個是任務執行的機制,不是一回事!
二、基本構件
- **Call Stack(調用棧)**:同步代碼入棧執行、執行完出棧。
- 宏任務(macrotask / task)隊列:例如
setTimeout、setInterval、setImmediate(Node)、DOM 事件、I/O 回調、UI 渲染觸發等。宏任務作為之前的一種籠統叫法,現代瀏覽器對這些任務做了更細的劃分,對他們都統稱為了 task,不同的任務具有不同的隊列。不過,微任務的概念一直被保留使用。
- 微任務(microtask / job)隊列:例如
Promise.then/catch/finally、async+await、queueMicrotask、MutationObserver(瀏覽器)、process.nextTick(Node)。 - **渲染/繪製階段(browser)**:在合適時機把更新繪製到屏幕(通常在 macrotask 完成並且 microtasks 已清空之後)。
主要為了後面對於任務執行和瀏覽器渲染順序的理解。
- **事件循環(event loop)**:不斷循環——執行一個 macrotask → 清空所有 microtasks → 執行渲染(若需要) → 下一個 macrotask。
三、瀏覽器裏的執行模型
循環的每一輪(tick)大致順序:
- 從 macrotask queue 取出一個任務並執行(例如頁面初始 script)。
- 當前任務執行完後,立即運行並清空 microtask queue(每出現一個 microtask,它會被加入隊列;直到隊列空才返回)。
microtasks 在同一輪裏可能不斷產生並被立即處理。
- 當 microtasks 清空後,會進行一次 渲染/繪製(如果需要)。
- 進入下一輪 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
解釋:
- 同步:
start、end - microtask:
promise - 渲染相關:
requestAnimationFrame在下一幀渲染前執行(在 microtasks 清空後,但通常在 macrotask 之前的 render 時機),所以raf在timeout之前 setTimeout是下一輪 macrotask,所以最後輸出。
四、Node.js(libuv)與瀏覽器的區別
Node 的底層是 libuv,事件循環分多個階段:
timers(處理 setTimeout/setInterval)pending callbacks(I/O 回調)idle, prepare(內部使用)poll(檢索新的 I/O 事件並執行)check(處理 setImmediate)close callbacks(socket close 等)
微任務(Promise callbacks)是在每個階段執行後 立即清空(microtask checkpoint);另外 Node 有 process.nextTick,其優先級甚至高於 microtasks(會在當前階段馬上執行,且會在 Promise microtasks 之前運行)。
我不想放很多面試題去講解,因為面試題是做不完的,而知識的核心重點就是這些。
上面的理論搞懂了,基本上相關面試題都可以做對。