理解事件迴圈(從瀏覽器端到node端)

海寧不想說話發表於2019-02-17

1.單執行緒的js

js區別於其他語言即在於它的單執行緒特性,考慮到它的主要執行環境(瀏覽器),這樣設計也是情理之中:

  • js在執行時頻繁操作DOM是很常見的業務需求,試想如果同時有多個事件同時在操作同一個DOM節點,瀏覽器怕是分分鐘要崩掉。
  • 雖然HTML5中提出Web Worker允許多個執行緒同時執行js程式碼,但其實質也只是在主執行緒中分出幾個子執行緒,並沒有改變js單執行緒的本質。並且在多個子執行緒中執行的js程式碼是不允許操作DOM的。

2.為什麼要有事件迴圈

事件驅動的js程式難免在程式設計時出現諸多事件,而依靠單執行緒來執行這些事件,若只是按部就班當一個事件執行完成後再執行另一個事件,那當遇到費時很長的比如ajax請求時,程式豈不是要死掉,依據網際網路行業的八秒原則,怕是使用者要跑的差不多了。遇到這種情況,如果是別的語言,還可以利用多執行緒將事件分發,避免造成阻塞。而js的解決方案則是非同步處理事件,提到非同步則是依賴於我們要談的事件迴圈(Event Loop)。下面我們會從瀏覽器和node環境來談各自的事件迴圈具體實現。

3.瀏覽器端

先從一張圖看一下瀏覽器事件迴圈的執行機制(圖片稍有不完整,會在後面解釋中補充)理解事件迴圈(從瀏覽器端到node端)


  • 執行棧:
    • js引擎在執行js程式碼時,會維護一個執行棧,這個棧用來存放要執行的事件。
    • 執行棧中只會存在一個事件,所謂js的執行過程不過就是各種事件不斷入棧->執行->出棧的過程罷了。
  • 同步/非同步事件
    • 記得前面有提到js對於事件的非同步處理方法,在這裡事件被分為同步事件和非同步事件。
    • 同步事件比如一些立馬執行的語句,比如很常見的console.log("我是同步的"),而非同步事件則比如一些ajax請求的回撥函式,一些滑鼠,鍵盤事件等。
  • 執行過程
    • js在執行時(從上到下,逐條執行)碰到同步事件則會壓入執行棧,然後被執行,出棧。而當遇到非同步事件則會被註冊進Event-Table中,待事件有結果(比如ajax請求完成,setTimeout到了延時的時間)時就會被推入事件佇列中,就是上圖中的callback queue。當同步任務執行完成後(即主執行緒中執行棧為空時),event loop 就會去檢查我們這裡的是事件佇列,若存在事件則依次進入執行棧被執行。
  • 說了這麼多,舉個栗子吧

    setTimeout(()=>{
      console.log('我是延時為3s的定時器')
    },3000)
    setTimeout(()=>{
      console.log('我是延時為0的定時器')
    },0)
    console.log('我是同步的')複製程式碼

    • 先來看執行效果

      我是同步的
      我是延時為0的定時器
      我是延時為3s的定時器複製程式碼

    • 執行過程:
      • 先碰到setTimeout(3s),非同步事件,將他註冊進event-table中。
      • 接下來是setTimeout(0s),同樣是非同步事件,註冊進event-table中。
      • 接著碰到console.log('我是同步的')識別為同步事件,進入執行棧,執行,之後出棧。
      • 此時我們的執行棧已經為空,event loop就會去事件佇列中去找是不是有待執行的非同步事件。此時延時為0s的定時器肯定已經在佇列中了,進入執行棧執行對應的回撥函式。接著3s時延時為3s的定時器也會進入事件佇列中,和上面的一樣被執行。(這裡我們看到延時的時間只是它被註冊進事件佇列的時間,而實際被執行的時間或許會因為事件過多而有延遲,畢竟執行棧中始終只會有一個事件被執行,資源有限,先到先得
    • 其實所謂事件迴圈可以理解成一個死迴圈,始終在執行棧於事件佇列之間交替檢查
  • 在上面,我們將事件分為同步事件和非同步事件,而實際中對非同步事件(也不能完全說是對非同步事件的劃分,後面解釋)有更細的劃分。
    • microTask(微事件)和macroTask(巨集事件)
      • 巨集事件:script(這裡經常指的是<script>標籤,作為程式入口,所以我們前面說是對非同步事件的劃分有點不嚴謹),setTimeoutsetInterval
      • 微事件:promise
    • 這裡主要是為了解決event loop在拉取事件時像下面這種情況的尷尬

      //event-table註冊了以下事件
      setTimeout(()=>{},0);
      new Promise(function (resolve,reject){
          resolve()
      }).then(()=>{
          //statement
      })
      //到底誰應該被先入隊複製程式碼

    • 所以在事件迴圈中將事件佇列分為巨集事件佇列和微事件佇列,非同步事件將在有結果時會被分發進各自的佇列。
    • 對於巨集事件和微事件的執行原則
      • 將以script為第一個巨集事件去開始第一輪迴圈。
      • 一次只執行一個巨集事件。
      • 執行完一個巨集事件後會清空此時微事件佇列中的所有事件,一次迴圈完成。
      • 去執行巨集事件佇列中的下一個事件...
    • 一張圖去理解(引用地址:https://juejin.im/post/59e85eebf265da430d571f89

理解事件迴圈(從瀏覽器端到node端)

  • 到這我們的瀏覽器端的事件迴圈就結束了,接下來是node環境中的事件迴圈,先來張圖過渡一下。(圖片來自李鍇的《新時期的node.js入門》)

    理解事件迴圈(從瀏覽器端到node端)

4.node端

  • 作為服務端的js執行環境,node在處理高併發的服務端需求時表現極為出色,而這一特性也於我們的事件迴圈息息相關。
  • 從前面的圖來看,node的事件迴圈是按階段來的,下面我們來分階段介紹
    • timers:這裡維護著一個專門針對setTimeout,setInterval的事件佇列。定時器會在有結果(到達延時時間時)被註冊進這個佇列中後面稱timer queue
    • IO callbacks:這個階段處理一些上一輪迴圈少數未執行的I/O回撥。
    • idle,prepare:內部實現,與程式碼中事件迴圈無關。
    • poll:這個階段可以看成整個事件迴圈的主導者,絕大部分事件迴圈在這個部分完成。具體過程後面專門介紹。
    • check:本階段主要維護針對setImmediate(在當前事件迴圈的結尾執行)的事件佇列(後面稱check queue)。
    • close callback:主要處理一些關閉連線的回撥。理解事件迴圈(從瀏覽器端到node端)


  • 主要來說poll階段
    • poll:這裡我們把他解釋為輪詢
    • poll主要幹兩件事:
      • 檢查timer維護的事件的事件佇列中是否有事件被分發進佇列
      • 執行poll階段自己維護的事件佇列
    • 進入poll階段時:
      • 檢查並執行poll事件佇列(後面稱poll queue)中的事件
      • poll queue為空時檢查check queue,若不為空進入check階段,執行。
      • 檢查timer queue是否為空,若不為空進入timer階段,執行。
  • 這裡有一個特例process.nextTick
    • 用來定義一個非同步事件,這個事件將在當前事件迴圈階段結束後執行,注意與setImmediate的區別,所以說如果這兩個同時存在於一輪迴圈中,process.nextTick總是在setImmediate之前執行
    • 這個方法定義的事件將被分發到nextTick queue中。
  • 再來提起我們很熟悉的巨集事件和微事件。
    • node中也將非同步事件分為巨集事件和微事件來管理執行順序,執行原則在最新的node標準中於瀏覽器一致,這裡有一張圖很清晰的解釋執行順序(圖片來源:https://juejin.im/post/5c337ae06fb9a049bc4cd218
    • 理解事件迴圈(從瀏覽器端到node端)

5.後記

根據個人理解以及整理得,有錯誤或者偏差敬請原諒,歡迎指正。


相關文章