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 前端開發工程師,目前在從事雲信金融線業務相關開發工作。