动态

详情 返回 返回

參透 JavaScript —— 圖解 Event Loop 事件循環 - 动态 详情

前言

本篇文章主要講解瀏覽器中事件循環(Event Loop) 那些事

單線程 JavaScript 中的同步和異步

同步任務是立即執行的任務,在調用棧(Call Stack)順序執行

異步任務則不同,它在同步任務沒完成之前,不會進入主線程,而是將對應回調函數註冊到隊列中,要理解這一步,我們先要知道任務隊列

任務隊列

在調用棧(Call Stack)中,如果遇到一個異步操作,那麼會將對應的回調函數註冊到任務隊列,並且,任務隊列會遵循先進先出的原則

不同的異步操作會進入到不同的任務隊列中

Image

任務隊列在一貫的説法中,會細分為微任務(Micro Task)和宏任務(Macro Task),並且微任務的優先級會比宏任務高

儘管當前W3C最新標準中,已無宏任務的概念,而是用微任務隊列、交互隊列、延時隊列等...,但目前使用微/宏任務概念來理解也並無問題

常見的微任務: Promise.thenPromise.catchMutaionObserver

常見宏任務:setTimeoutsetInterval、I/O 操作、DOM 事件、script 標籤

注意,每個宏任務執行時,會先完整執行其所有同步代碼,然後清空當前微任務隊列(包括嵌套產生的微任務),最後才會處理下一個宏任務

圖解事件循環機制

事件循環是用來處理異步任務的核心機制

用一句話來概括就是,在同步任務執行完後,回調棧會不斷從任務隊列中讀取回調函數並壓入棧中執行,這個運作流程機制就被稱為事件循環(Event Loop)

  1. 所有同步代碼直接進入調用棧,按順序執行,直到調用棧清空
  2. 調用棧清空後,查找任務隊列是否有任務
  3. 如果有,遵循先進先出的規則將最老的回調(最先進入任務隊列的回調)推入棧中執行
  4. 重複上述流程,形成事件循環

第二、三步中,我們説會查找任務隊列是否有任務並推入執行,由於微任務隊列有很高的優先級,所以查找的順序展開來説是:

  • 優先檢查微任務隊列,如果隊列不為空,遵循先進先出的規則推入棧中執行
  • 當微任務隊列清空後,當前事件循環就走完了
  • 然後從宏任務中取出一個任務(最先進入的),推入調用棧執行,就進入下一輪循環

圖示

<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):

  1. 同步任務

    • 執行 console.log('task1'),輸出 task1
    • 遇到 setTimeout ,當 time 時間結束時將其回調函數註冊並放入宏任務隊列
    • 執行 new Promise 中的執行器函數,輸出 task3
    • 執行器函數中的 resolve 方法執行,將 then 的回調函數註冊到微任務隊列
    • 執行 console.log('task5'),輸出 task5
    • 至此,第一輪的同步任務執行完畢
  2. 微任務隊列

    • 執行 then 的回調函數,輸出 task6
    • 微任務隊列清空

圖示:

Image

第二輪事件循環(宏任務2:setTimeout 註冊的回調)

  1. 同步任務

    • 執行 setTimeout 註冊的回調,創建了一個 Promise
    • 執行 Promise 執行器函數中的 console.log('task2') ,輸出 task2
    • 執行 resolve 方法,將第一個 then 的回調函數註冊到微任務隊列
    • 至此,同步任務執行完畢
  2. 微任務隊列

    • 執行第一個 then 的回調函數,輸出 task4
    • then 的鏈式調用,註冊第二個 then 的回調函數
    • 執行 第二個 then 的回調函數,輸出 task7
    • 微任務隊列清空

圖示:

Image

最終輸出順序是:task1 task3 task5 task6 task2 task4 task7

當面對一段包含同步、異步的代碼段時,能夠清楚明白其運行機制,知道輸出順序,即可算掌握

總結

文章開篇我們圍繞同步任務和異步任務做了介紹:

  • 同步任務按順序,在棧中順序執行
  • 異步任務的回調函數註冊到任務隊列

引出了任務隊列的存在後,講解了任務隊列細分為微任務和宏任務,微任務隊列的優先級最高

最後是本文核心,理解事件循環,一句話來概括就是:在同步任務執行完後,回調棧會不斷從任務隊列中讀取回調函數並壓入棧中執行

這裏要注意,結合實踐代碼來分析才能真正理解掌握

參考資料

  • JavaScript 運行機制詳解:再談Event Loop - 阮一峯
  • 詳解JavaScript中的Event Loop(事件循環)機制

參透JavaScript系列

本文已收錄至《參透JavaScript系列》,全文地址:我的 GitHub 博客 | 掘金專欄

對你有幫助的話,歡迎 Star

交流討論

對文章內容有任何疑問、建議,或發現有錯誤,歡迎交流和指正

user avatar honwhy 头像 Leesz 头像 guochenglong 头像 zs_staria 头像 jingdongkeji 头像 aqiongbei 头像 longlong688 头像 inslog 头像 solvep 头像 dunizb 头像 febobo 头像 imba97 头像
点赞 102 用户, 点赞了这篇动态!
点赞

Add a new 评论

Some HTML is okay.