JavaScript 之事件迴圈(Event Loop)

語言: CN / TW / HK

導讀:學過 JavaScript(下文簡稱 JS) 的都知道它是一門單執行緒的、非阻塞的指令碼語言。單執行緒意味著,JS 程式碼在執行的任何時候,都只有一個主執行緒來處理所有的任務,這也就意味著 JS 無法進行多執行緒程式設計,但是 JS 當中卻有著無處不在的非同步概念,我們如何理解呢?理解非同步和非阻塞靠的就是 Event Loop(事件迴圈),本文就圍繞 JS 執行緒、同步非同步、任務佇列等方面講解事件迴圈(Event Loop)。

JS 執行緒

為了我們更方便容易瞭解事件迴圈,在此之前我們先簡單瞭解下什麼叫做 JS 執行緒。如瀏覽器的渲染程序是多執行緒的,主要有以下幾個執行緒:

  • JS 引擎執行緒(主執行緒):負責解析 JS 指令碼,執行程式碼。

     

  • GUI 渲染執行緒:負責渲染瀏覽器介面,解析 HTML、CSS、構 DOM 樹和 RenderObject 樹,佈局和繪製等,當介面需要重繪(Repaint)或由於某種操作引發迴流 (reflow) 時,該執行緒就會執行。

     

  • 定時器觸發執行緒 (setTimeout):瀏覽器定時計數器並不是由 JS 引擎計數的,因為 JS 引擎是單執行緒的, 如果處於阻塞執行緒狀態就會影響記計時的準確,因此通過單獨執行緒來計時並觸發定時,在計時完畢後,新增到事件佇列中,等待 JS 引擎空閒後執行。

     

  • http 請求執行緒(ajax):XMLHttpRequest 連線後,通過瀏覽器新開一個執行緒請求,當檢測到狀態變更時,如果同時設定有回撥函式,非同步執行緒就產生狀態變更事件,將這個回撥再放入事件佇列中,再由 JS 引擎執行。

     

  • 瀏覽器事件觸發執行緒 (onclick):歸屬於瀏覽器而不是 JS 引擎,用來控制事件迴圈,可以這麼理解:JS 引擎自己都忙不過來,需要瀏覽器另開執行緒協助。

     

  • 主執行緒和渲染執行緒互斥 :JS 引擎執行緒與 GUI 渲染執行緒是互斥的,當 JS 引擎執行時 GUI 執行緒會被掛起(相當於被凍結了),GUI 更新會被儲存在一個佇列中等到 JS 引擎空閒時立即被執行。

瀏覽器核心

  • EventLoop 輪詢處理執行緒:我們可以把它理解為一箇中介,在主執行緒、非同步執行緒與訊息佇列三者之間進行交流與溝通。如下圖所示:從主執行緒那裡順時針的看,整個的流程是迴圈往復的。只有當主執行緒的同步程式碼都執行完了,才會去佇列裡看看還有什麼要執行的。

主執行緒把 setTimeout、ajax、dom.onclick 分別給三個執行緒,他們之間有些不同。

1、對於 setTimeout 程式碼,定時器觸發執行緒在接收到程式碼時就開始計時,時間到了將回調函式扔進訊息佇列。

2、對於 ajax 程式碼,http 非同步執行緒立即發起 http 請求,請求成功後將回調函式扔進訊息佇列。

3、對於 dom.onclick,瀏覽器事件執行緒會先監聽 dom,直到 dom 被點選了,才將回撥函式扔進訊息佇列。

同步與非同步

JS 分為同步任務和非同步任務:

  • 同步任務:立即執行的任務佇列,比如一個簡單的函式;

     

  • 非同步任務:請求介面傳送 ajax,傳送 promise,或時間計時器等等;

任務佇列(Event Queue)

什麼是任務佇列呢?可以理解為一個靜態的佇列儲存結構,遵循先進先出原則:同步任務會立刻執行,進入到主執行緒當中;非同步任務會被放到任務佇列(Event Queue)當中。

巨集任務佇列和微任務佇列

巨集任務(MacroTask):整體程式碼 Script、UI 渲染、setTimeout、setInterval、setImmediate(Node.js 環境)。

微任務(MicroTask):Promise.then()、catch、finally。

不同點:event loop 裡 MacroTask 佇列可能有多個,MicroTask 佇列只有一個。

 

 

MicroTask 優先於 MacroTask 執行,所以如果有需要優先執行的邏輯,放入 MicroTask 佇列會比 MacroTask 更早的被執行。

下面幾個程式碼例子可以讓我們充分的瞭解各個任務之間的執行順序:

執行棧

 

MacroTask 和 MicroTask 都是推入棧中執行的。JS 是單執行緒,也就是說只有一個主執行緒,主執行緒有一個棧,每一個函式執行的時候,都會生成新的執行上下文,執行上下文會包含一些當前函式的引數、區域性變數之類的資訊,它會被推入棧中,正在執行的上下文始終處於棧的頂部。當函式執行完後,它的執行上下文會從棧彈出。

總結

同步和非同步任務分別進入不同的執行環境, 先執行同步任務,把非同步任務放入迴圈隊列當中,等待同步任務執行完,再執行佇列中的非同步任務。非同步任務先執行微觀任務,再執行巨集觀任務。一直這樣迴圈,反覆執行,就是我們說的 Event Loop (事件迴圈)。

事件迴圈是 JS 這門語言中非常重要且基礎的概念。讓我們可以清楚的瞭解事件迴圈的執行順序和每一個階段的特點,可以使我們對一段非同步程式碼的執行順序有一個清晰的認識,從而減少程式碼執行的不確定性。合理的使用各種延遲事件的方法,有助於程式碼更好的按照其優先順序去執行。

如果在閱讀期間您發現了文章中的一些問題,歡迎在留言中提出,感謝您閱讀此文章。

作者介紹 

倪萌,網易雲信 web 前端開發工程師,目前在從事雲信金融線業務相關開發工作。