Js 的事件迴圈(Event Loop)機制以及例項講解

OBKoro1發表於2018-06-19

前言

大家都知道js是單執行緒的指令碼語言,在同一時間,只能做同一件事,為了協調事件、使用者互動、指令碼、UI渲染和網路處理等行為,防止主執行緒阻塞,Event Loop方案應運而生...

個人部落格瞭解一下:obkoro1.com


為什麼js是單執行緒?

js作為主要執行在瀏覽器的指令碼語言,js主要用途之一是操作DOM。

在js高程中舉過一個栗子,如果js同時有兩個執行緒,同時對同一個dom進行操作,這時瀏覽器應該聽哪個執行緒的,如何判斷優先順序?

為了避免這種問題,js必須是一門單執行緒語言,並且在未來這個特點也不會改變。


執行棧與任務佇列

因為js是單執行緒語言,當遇到非同步任務(如ajax操作等)時,不可能一直等待非同步完成,再繼續往下執行,在這期間瀏覽器是空閒狀態,顯而易見這會導致巨大的資源浪費。

執行棧

當執行某個函式、使用者點選一次滑鼠,Ajax完成,一個圖片載入完成等事件發生時,只要指定過回撥函式,這些事件發生時就會進入任務佇列中,等待主執行緒讀取,遵循先進先出原則。

執行任務佇列中的某個任務,這個被執行的任務就稱為執行棧。

主執行緒

要明確的一點是,主執行緒跟執行棧是不同概念,主執行緒規定現在執行執行棧中的哪個事件。

主執行緒迴圈:即主執行緒會不停的從執行棧中讀取事件,會執行完所有棧中的同步程式碼。

當遇到一個非同步事件後,並不會一直等待非同步事件返回結果,而是會將這個事件掛在與執行棧不同的佇列中,我們稱之為任務佇列(Task Queue)。

當主執行緒將執行棧中所有的程式碼執行完之後,主執行緒將會去檢視任務佇列是否有任務。如果有,那麼主執行緒會依次執行那些任務佇列中的回撥函式。

不太理解的話,可以執行一下下面的程式碼,或者點選一下這個demo

結果是當a、b、c函式都執行完成之後,三個setTimeout才會依次執行。

let a = () => {
  setTimeout(() => {
    console.log('任務佇列函式1')
  }, 0)
  for (let i = 0; i < 5000; i++) {
    console.log('a的for迴圈')
  }
  console.log('a事件執行完')
}
let b = () => {
  setTimeout(() => {
    console.log('任務佇列函式2')
  }, 0)
  for (let i = 0; i < 5000; i++) {
    console.log('b的for迴圈')
  }
  console.log('b事件執行完')
}
let c = () => {
  setTimeout(() => {
    console.log('任務佇列函式3')
  }, 0)
  for (let i = 0; i < 5000; i++) {
    console.log('c的for迴圈')
  }
  console.log('c事件執行完')
}
a();
b();
c();
// 當a、b、c函式都執行完成之後,三個setTimeout才會依次執行
複製程式碼

js 非同步執行的執行機制。

  1. 所有任務都在主執行緒上執行,形成一個執行棧。
  2. 主執行緒之外,還存在一個"任務佇列"(task queue)。只要非同步任務有了執行結果,就在"任務佇列"之中放置一個事件。
  3. 一旦"執行棧"中的所有同步任務執行完畢,系統就會讀取"任務佇列"。那些對應的非同步任務,結束等待狀態,進入執行棧並開始執行。
  4. 主執行緒不斷重複上面的第三步

巨集任務與微任務:

非同步任務分為 巨集任務(macrotask) 與 微任務 (microtask),不同的API註冊的任務會依次進入自身對應的佇列中,然後等待 Event Loop 將它們依次壓入執行棧中執行。

巨集任務(macrotask):

script(整體程式碼)、setTimeout、setInterval、UI 渲染、 I/O、postMessage、 MessageChannel、setImmediate(Node.js 環境)

微任務(microtask):

Promise、 MutaionObserver、process.nextTick(Node.js環境)

Event Loop(事件迴圈):

Event Loop(事件迴圈)中,每一次迴圈稱為 tick, 每一次tick的任務如下:

  • 執行棧選擇最先進入佇列的巨集任務(通常是script整體程式碼),如果有則執行
  • 檢查是否存在 Microtask,如果存在則不停的執行,直至清空 microtask 佇列
  • 更新render(每一次事件迴圈,瀏覽器都可能會去更新渲染)
  • 重複以上步驟

巨集任務 > 所有微任務 > 巨集任務,如下圖所示:

Js 的事件迴圈(Event Loop)機制以及例項講解

從上圖我們可以看出:

  1. 將所有任務看成兩個佇列:執行佇列與事件佇列。
  2. 執行佇列是同步的,事件佇列是非同步的,巨集任務放入事件列表,微任務放入執行佇列之後,事件佇列之前。
  3. 當執行完同步程式碼之後,就會執行位於執行列表之後的微任務,然後再執行事件列表中的巨集任務

上面提到的demo結果可以這麼理解:先執行script巨集任務,執行完了之後,再執行其他兩個定時器巨集任務。


面試題實踐

下面這個題,很多人都應該看過/遇到過,重新來看會不會覺得清晰很多:

    // 執行順序問題,考察頻率挺高的,先自己想答案**
    setTimeout(function () {
        console.log(1);
    });
    new Promise(function(resolve,reject){
        console.log(2)
        resolve(3)
    }).then(function(val){
        console.log(val);
    })
    console.log(4);
複製程式碼

根據本文的解析,我們可以得到:

  1. 先執行script同步程式碼

     先執行new Promise中的console.log(2),then後面的不執行屬於微任務
     然後執行console.log(4)
    複製程式碼
  2. 執行完script巨集任務後,執行微任務,console.log(3),沒有其他微任務了。

  3. 執行另一個巨集任務,定時器,console.log(1)。

根據本文的內容,可以很輕鬆,且有理有據的猜出寫出正確答案:2,4,3,1.


結語

類似上文的面試題還有很多,實則都大同小異,只要掌握了事件迴圈的機制,這些問題都會變得很簡單。

文章如有不正確的地方歡迎各位路過的大佬鞭策!希望大家看完可以有所收穫,喜歡的話,趕緊點波訂閱關注/喜歡。

看完的朋友可以點個喜歡/關注,您的支援是對我最大的鼓勵。

個人blog and 掘金個人主頁,如需轉載,請放上原文連結並署名。碼字不易,感謝支援!

如果喜歡本文的話,歡迎關注我的訂閱號,漫漫技術路,期待未來共同學習成長。

Js 的事件迴圈(Event Loop)機制以及例項講解

以上2018.6.16

參考資料:

詳解JavaScript中的Event Loop(事件迴圈)機制

JavaScript中的事件迴圈 Event Loop

JavaScript 執行機制詳解:再談Event Loop

相關文章