事件循環(huán)
事件循環(huán)是 JavaScript 引擎中一個至關(guān)重要的部分,它使得 JavaScript 能夠在單線程中以非阻塞的方式執(zhí)行。要理解事件循環(huán)是如何工作的,我們需要首先了解一些關(guān)鍵概念,包括調(diào)用堆棧、任務(wù)、微任務(wù)及它們各自所在的隊列。
調(diào)用堆棧(Call Stack)
調(diào)用堆棧是一種數(shù)據(jù)結(jié)構(gòu),用于跟蹤正在執(zhí)行的 JavaScript 代碼中的函數(shù)調(diào)用。顧名思義,調(diào)用堆棧是一個堆棧,即內(nèi)存中的后進(jìn)先出(LIFO)數(shù)據(jù)結(jié)構(gòu)。每個被執(zhí)行的函數(shù)都表示為調(diào)用堆棧中的一個幀,會在前一個函數(shù)之上入棧。
函數(shù)
foo()
被壓入調(diào)用堆棧。函數(shù)
foo()
被執(zhí)行并從調(diào)用堆棧中彈出。函數(shù)
console.log('foo')
被推送到調(diào)用堆棧上。函數(shù)
console.log('foo')
被執(zhí)行并從調(diào)用堆棧中彈出。函數(shù)
bar()
被壓入調(diào)用堆棧。函數(shù)
bar()
被執(zhí)行并從調(diào)用堆棧中彈出。函數(shù)
console.log('bar')
被推送到調(diào)用堆棧上。函數(shù)
console.log('bar')
被執(zhí)行并從調(diào)用堆棧中彈出。調(diào)用堆?,F(xiàn)在為空。
任務(wù)(Task)與任務(wù)隊列(Task Queue)
任務(wù)是預(yù)定的、同步執(zhí)行的代碼塊。在執(zhí)行過程中,它們對調(diào)用堆棧具有獨占訪問權(quán)限,并且可以將其他任務(wù)排入隊列。任務(wù)之間可執(zhí)行渲染更新。任務(wù)存儲在任務(wù)隊列中,并等待著相關(guān)函數(shù)執(zhí)行。任務(wù)隊列是一個先進(jìn)先出(FIFO)的數(shù)據(jù)結(jié)構(gòu)。典型的任務(wù)示例包括事件監(jiān)聽器回調(diào)函數(shù)以及 setTimeout() 的回調(diào)。
微任務(wù)(Microtask)與微任務(wù)隊列(Microtask Queue)
微任務(wù)與任務(wù)類似,因為它們也是預(yù)定的同步執(zhí)行的代碼塊,并在執(zhí)行過程中對調(diào)用堆棧具有獨占訪問權(quán)限。此外,它們還存儲在自己的先進(jìn)先出(FIFO)數(shù)據(jù)結(jié)構(gòu)中,即微任務(wù)隊列。然而,微任務(wù)與任務(wù)的區(qū)別在于,它們會在當(dāng)前任務(wù)執(zhí)行完畢之后以及重新渲染之前立即執(zhí)行。典型的微任務(wù)示例包括 Promise 回調(diào)和 MutationObserver 回調(diào)。
事件循環(huán)(Event Loop)
事件循環(huán)是一個持續(xù)運行并檢查調(diào)用堆棧是否為空的過程。它通過將任務(wù)和微任務(wù)逐個放入調(diào)用堆棧中來處理它們,并對渲染過程進(jìn)行控制。事件循環(huán)的核心包含四個關(guān)鍵步驟:
腳本評估:逐個執(zhí)行腳本內(nèi)的代碼,直到調(diào)用堆棧為空。
任務(wù)處理:選擇任務(wù)隊列中的第一個任務(wù),然后執(zhí)行它,一直執(zhí)行到調(diào)用堆棧為空。
微任務(wù)處理:從微任務(wù)隊列中取出第一個微任務(wù)執(zhí)行,直至調(diào)用堆棧為空,重復(fù)此過程直至微任務(wù)隊列為空。
渲染:重新渲染 UI 界面,并循環(huán)回到步驟 2。
實際示例
讓我們逐步分析這個過程:
調(diào)用堆棧最初為空。事件循環(huán)開始評估腳本。
console.log()
入棧并執(zhí)行,輸出 "Script start"。setTimeout()
入棧并執(zhí)行。該操作會在任務(wù)隊列中為其回調(diào)函數(shù)創(chuàng)建一個新任務(wù)。Promise.prototype.resolve()
入棧并執(zhí)行,然后依次調(diào)用Promise.prototype.then()
。Promise.prototype.then()
入棧并執(zhí)行。這會在微任務(wù)隊列中為其回調(diào)函數(shù)創(chuàng)建一個新的微任務(wù)。console.log()
入棧并執(zhí)行,輸出 "Script end"。事件循環(huán)已完成其當(dāng)前任務(wù)(評估腳本),接著開始處理微任務(wù)隊列中的第一個微任務(wù),即在步驟 5 中排入隊列的
Promise.prototype.then()
的回調(diào)。console.log()
入棧并執(zhí)行,輸出 "Promise.then() #1"。Promise.prototype.then()
入棧并執(zhí)行。這會在微任務(wù)隊列中為其回調(diào)函數(shù)創(chuàng)建一個新條目。事件循環(huán)檢查微任務(wù)隊列。因為它還不為空,所以會繼續(xù)執(zhí)行隊列中的第一個微任務(wù)——在步驟 10 中排入隊列的
Promise.prototype.then()
的回調(diào)。console.log()
入棧并執(zhí)行,輸出 "Promise.then() #2"。如果有的話,在此處進(jìn)行重新渲染。
由于微任務(wù)隊列已經(jīng)為空,事件循環(huán)將轉(zhuǎn)到任務(wù)隊列,并執(zhí)行第一個任務(wù)。這是在步驟 3 中排入隊列的
setTimeout()
的回調(diào)。console.log()
入棧并執(zhí)行,輸出 "setTimeout()"。如果有的話,在此處進(jìn)行重新渲染。
調(diào)用堆?,F(xiàn)在為空。
總結(jié)
事件循環(huán)負(fù)責(zé)執(zhí)行 JavaScript 代碼。首先處理腳本評估和執(zhí)行,然后處理任務(wù)和微任務(wù)。
任務(wù)和微任務(wù)都是預(yù)定的同步代碼塊。它們逐個執(zhí)行,并分別放置在任務(wù)隊列和微任務(wù)隊列中。
調(diào)用堆棧跟蹤 JavaScript 中的函數(shù)調(diào)用。
每次執(zhí)行微任務(wù)時,都需要清空微任務(wù)隊列,然后才能進(jìn)行下一個任務(wù)的處理。
渲染工作發(fā)生在任務(wù)之間,但不會發(fā)生在微任務(wù)之間。
附加說明
事件循環(huán)的腳本評估步驟本身與任務(wù)類似,即按順序執(zhí)行直至結(jié)束。
setTimeout() 的第二個參數(shù)表示執(zhí)行前的最短延遲時間,而非保證時間。這是因為任務(wù)會按順序執(zhí)行,而微任務(wù)可能在此期間插入執(zhí)行。
在 Node.js 中,事件循環(huán)的行為類似于在瀏覽器中的行為。但它們之間也存在一些差異,最明顯的區(qū)別是不存在渲染步驟。
較舊版本的瀏覽器可能不會完全遵循這種操作順序,因此任務(wù)和微任務(wù)可能會以不同的順序執(zhí)行。