前言
本篇文章主要講解瀏覽器中事件循環(Event Loop) 那些事
單線程 JavaScript 中的同步和異步
同步任務是立即執行的任務,在調用棧(Call Stack)順序執行
異步任務則不同,它在同步任務沒完成之前,不會進入主線程,而是將對應回調函數註冊到隊列中,要理解這一步,我們先要知道任務隊列
任務隊列
在調用棧(Call Stack)中,如果遇到一個異步操作,那麼會將對應的回調函數註冊到任務隊列,並且,任務隊列會遵循先進先出的原則
不同的異步操作會進入到不同的任務隊列中
任務隊列在一貫的説法中,會細分為微任務(Micro Task)和宏任務(Macro Task),並且微任務的優先級會比宏任務高
儘管當前W3C最新標準中,已無宏任務的概念,而是用微任務隊列、交互隊列、延時隊列等...,但目前使用微/宏任務概念來理解也並無問題
常見的微任務: Promise.then、Promise.catch、MutaionObserver
常見宏任務:setTimeout、setInterval、I/O 操作、DOM 事件、script 標籤
注意,每個宏任務執行時,會先完整執行其所有同步代碼,然後清空當前微任務隊列(包括嵌套產生的微任務),最後才會處理下一個宏任務
圖解事件循環機制
事件循環是用來處理異步任務的核心機制
用一句話來概括就是,在同步任務執行完後,回調棧會不斷從任務隊列中讀取回調函數並壓入棧中執行,這個運作流程機制就被稱為事件循環(Event Loop)
- 所有同步代碼直接進入調用棧,按順序執行,直到調用棧清空
- 調用棧清空後,查找任務隊列是否有任務
- 如果有,遵循先進先出的規則將最老的回調(最先進入任務隊列的回調)推入棧中執行
- 重複上述流程,形成事件循環
第二、三步中,我們説會查找任務隊列是否有任務並推入執行,由於微任務隊列有很高的優先級,所以查找的順序展開來説是:
- 優先檢查微任務隊列,如果隊列不為空,遵循先進先出的規則推入棧中執行
- 當微任務隊列清空後,當前事件循環就走完了
- 然後從宏任務中取出一個任務(最先進入的),推入調用棧執行,就進入下一輪循環
圖示
<img width="887" alt="Image" src="https://github.com/user-attachments/assets/74389d50-b281-48ce-ae5e-f8e6375aeeec" />
理論總是抽象的,我們來舉個實際的例子,你可以先思考一下這段代碼的輸出順序
console.log('task1')
setTimeout(()=>{
new Promise((resolve,reject)=>{
console.log('task2')
resolve()
}).then(()=>{
console.log('task4')
}).then(()=>{
console.log('task7')
})
},0)
new Promise((resolve,reject)=>{
console.log('task3')
resolve()
}).then(()=>{
console.log('task6')
})
console.log('task5')
逐步分析這段代碼:
第一輪事件循環(宏任務1: script):
-
同步任務
- 執行
console.log('task1'),輸出task1 - 遇到
setTimeout,當time時間結束時將其回調函數註冊並放入宏任務隊列 - 執行
new Promise中的執行器函數,輸出task3 - 執行器函數中的
resolve方法執行,將then的回調函數註冊到微任務隊列 - 執行
console.log('task5'),輸出task5 - 至此,第一輪的同步任務執行完畢
- 執行
-
微任務隊列
- 執行
then的回調函數,輸出task6 - 微任務隊列清空
- 執行
圖示:
第二輪事件循環(宏任務2:setTimeout 註冊的回調)
-
同步任務
- 執行
setTimeout註冊的回調,創建了一個Promise - 執行
Promise執行器函數中的console.log('task2'),輸出task2 - 執行
resolve方法,將第一個then的回調函數註冊到微任務隊列 - 至此,同步任務執行完畢
- 執行
-
微任務隊列
- 執行第一個
then的回調函數,輸出task4 then的鏈式調用,註冊第二個then的回調函數- 執行 第二個
then的回調函數,輸出task7 - 微任務隊列清空
- 執行第一個
圖示:
最終輸出順序是:task1 task3 task5 task6 task2 task4 task7
當面對一段包含同步、異步的代碼段時,能夠清楚明白其運行機制,知道輸出順序,即可算掌握
總結
文章開篇我們圍繞同步任務和異步任務做了介紹:
- 同步任務按順序,在棧中順序執行
- 異步任務的回調函數註冊到任務隊列
引出了任務隊列的存在後,講解了任務隊列細分為微任務和宏任務,微任務隊列的優先級最高
最後是本文核心,理解事件循環,一句話來概括就是:在同步任務執行完後,回調棧會不斷從任務隊列中讀取回調函數並壓入棧中執行
這裏要注意,結合實踐代碼來分析才能真正理解掌握
參考資料
- JavaScript 運行機制詳解:再談Event Loop - 阮一峯
- 詳解JavaScript中的Event Loop(事件循環)機制
參透JavaScript系列
本文已收錄至《參透JavaScript系列》,全文地址:我的 GitHub 博客 | 掘金專欄
對你有幫助的話,歡迎 Star
交流討論
對文章內容有任何疑問、建議,或發現有錯誤,歡迎交流和指正