我是這樣理解EventLoop的

哇喔WEB發表於2021-01-03

我是這樣理解EventLoop的

在這裡插入圖片描述

一、前言

  眾所周知,在使用javascript時,經常需要考慮程式中存在非同步的情況,如果對非同步考慮不周,很容易在開發中出現技術錯誤和業務錯誤。作為一名合格的javascript使用者,瞭解非同步的存在和執行機制十分重要且有必要;那麼,非同步究竟是何方神聖呢?我們不得不提Event Loop:也叫做事件迴圈,是指瀏覽器或Node環境的一種解決javaScript單執行緒執行時不會阻塞的一種機制,也就是實現非同步的原理。作為一種單執行緒語言,javascript本身是沒有非同步這一說法的,是由其宿主環境提供的(EventLoop優秀文章網上有很多,這篇文章是自己的整合和理解)。
注意:Event Loop 並不是在 ECMAScript 標準中定義的,而是在 HTML 標準中定義的;

二、Event Loop知識鋪墊

  javascript程式碼執行時,任務被分為兩種,巨集任務(MacroTask/Task)微任務(MircoTask)Event Loop在執行和協調各種任務時也將任務佇列分為Task QueueMircoTak Queue分別對應管理巨集任務(MacroTask/Task)微任務(MircoTask);作為佇列,Task QueueMircoTak Queue也具備佇列特性:先進先出(FIFO—first in first out)

1、微任務(MircoTask)

  在 HTML 標準中,並沒有明確規定 Microtask,但是實際開發中包含以下四種:

  • Promise中的then、catch、finally(原理參考:【js進階】手撕Promise,一碼一解析 包懂
  • MutationObserver(監視 DOM 變動的API,詳情參考MDN
  • Object.observe(廢棄:監聽標準物件的變化)
  • Process.nextTick(Node環境,通常也被認為是微任務)

2、巨集任務(MacroTask/Task)

  基本上,我們將javascript中非微任務(MircoTask)的所有任務都歸為巨集任務,比如:

  • script中全部程式碼
  • DOM操作
  • 使用者互動操作
  • 所有的網路請求
  • 定時器相關的 setTimeout、setInterval 等
  • ···

3、javascript runtime

  javascript runtime:為 JavaScript 提供一些物件或機制,使它能夠與外界互動,是javascript的執行環境。javascript執行時會建立一個main thread主執行緒call-stack 呼叫棧(執行棧,遵循後進先出的規則)所有的任務都會被放到呼叫棧/執行棧等待主執行緒執行。其執行機制如下:
在這裡插入圖片描述

  • 1)主執行緒自上而下依次執行所有程式碼;
  • 2)同步任務直接進入到主執行緒被執行;
  • 3)非同步任務進入到Event Table,當非同步任務有結果後,將相對應的回撥函式進行註冊,放入Event Queue
  • 4)主執行緒任務執行完空閒下來後,從Event Queue(FIFO)中讀取任務,放入主執行緒執行;
  • 5)放入主執行緒的Event Queue任務繼續從第一步開始,如此迴圈執行;
    上述步驟執行過程就是我們所說的事件迴圈(Event Loop),上圖展示了事件迴圈中的一個完整迴圈過程。

三、瀏覽器環境的Event Loop

  不同的執行環境中,Event Loop的執行機制是不同的;例如Chrome 和 Node.js 都使用了 V8 Engine:V8 實現並提供了 ECMAScript 標準中的所有資料型別、操作符、物件和方法(注意並沒有 DOM)。但它們的 Runtime 並不一樣:Chrome 提供了 window、DOM,而 Node.js 則是 require、process 等等。我們在瞭解瀏覽器中Event Loop的具體表現前需要先整理同步、非同步、微任務、巨集任務之間的關係!

1、同步、非同步 和 巨集任務、微任務

  看到這裡,可能會有很多疑惑:同步非同步很好理解,巨集任務微任務上面也進行了分類,但是當他們四個在一起後就感覺很混亂了,冥冥之中覺得同步非同步和巨集任務微任務有內在聯絡,但是他們之間有聯絡嗎?又是什麼聯絡呢?網上有的文章說巨集任務就是同步的,微任務就是非同步的 這種說法明顯是錯的!
  其實我更願意如此描述:巨集任務和微任務是相對而言的,根據程式碼執時迴圈的先後,將程式碼執行分層理解,在每一層(一次)的事件迴圈中,首先整體程式碼塊看作一個巨集任務,巨集任務中的 Promise(then、catch、finally)、MutationObserver、Process.nextTick就是該巨集任務層的微任務;巨集任務中的同步程式碼進入主執行緒中立即執行的,巨集任務中的非微任務非同步執行程式碼將作為下一次迴圈的巨集任務時進入呼叫棧等待執行的;此時,呼叫棧中等待執行的佇列分為兩種,優先順序較高先執行的本層迴圈微任務佇列(MicroTask Queue),和優先順序低的下層迴圈執行的巨集任務佇列(MacroTask Queue)!
注意:每一次/層迴圈,都是首先從巨集任務開始,微任務結束;
在這裡插入圖片描述

2、簡單例項分析

上面的描敘相對拗口,結合程式碼和圖片分析理解:

在這裡插入圖片描述

  答案暫時不給出,我們先進行程式碼分析:這是一個簡單而典型的雙層迴圈事件迴圈執行案例,在這個迴圈中可以按照以下步驟進行分析:

  • 1、首先區分出該層巨集任務的範圍(整個程式碼);
  • 2、區分巨集任務同步程式碼非同步程式碼
    同步程式碼:console.log('script start');console.log('enter promise');console.log('script end');
    非同步程式碼塊:setTimeoutPromise的then注意Promise中只有then、catch、finally的執行需要等到結果,Promise傳入的回撥函式屬於同步執行程式碼);
  • 3、在非同步中找出同層的微任務(程式碼中的Promise的then)和下層事件迴圈的巨集任務(程式碼中的setTimeout
  • 4、巨集任務同步程式碼優先進入主執行緒,按照自上而下順序執行完畢;
    輸出順序為:
//同步程式碼執行輸出
script start
enter promise
script end
  • 5、當主執行緒空閒時,執行該層的微任務
//同層微任務佇列程式碼執行輸出
promise then 1
promise then 2
  • 6、首層事件迴圈結束,進入第二層事件迴圈(setTimeout包含的執行程式碼,只有一個同步程式碼)
//第二層巨集任務佇列程式碼執行輸出
setTimeout

綜合分析最終得出資料結果為:

//首層巨集任務程式碼執行輸出
script start
enter promise
script end
//首層微任務佇列程式碼執行輸出
promise then 1
promise then 2
//第二層巨集任務佇列程式碼執行輸出
setTimeout

3、複雜案例分析

  那麼,你是否已經瞭解上述執行過程了呢?如果完全理解上述例項,說明你已經大概知道瀏覽器中Event Loop的執行機制,但是,要想知道自己是不是完全明白,不妨對於下列多迴圈的事件迴圈進行分析檢驗,給出你的結果:

console.log('1');

setTimeout(function() {
    console.log('2');
    new Promise(function(resolve) {
        console.log('3');
        resolve();
    }).then(function() {
        console.log('4')
    })
    setTimeout(function() {
	    console.log('5');
	    new Promise(function(resolve) {
	        console.log('6');
	        resolve();
	    }).then(function() {
	        console.log('7')
	    })
	})
	console.log('14');
})

new Promise(function(resolve) {
    console.log('8');
    resolve();
}).then(function() {
    console.log('9')
})

setTimeout(function() {
    console.log('10');
    new Promise(function(resolve) {
        console.log('11');
        resolve();
    }).then(function() {
        console.log('12')
    })
})
console.log('13')

分析:如下圖草稿所示,左上角標a為巨集任務佇列,左上角標i為微任務佇列,同一層迴圈中,本層巨集任務先執行,再執行微任務;本層巨集任務中的非微任務非同步程式碼塊作為下層迴圈的巨集任務進入下次迴圈,如此迴圈執行;
在這裡插入圖片描述

如果你的與下面的結果一致,恭喜你瀏覽器環境的Event Loop你已經完全掌握,那麼請開始下面的學習:

1->8->13->9->2->3->14->4->10->11->12->5->6->7

四、Node 環境下的 Event Loop

  在Node環境下,瀏覽器的EventLoop機制並不適用,切記不能混為一談。這裡借用網上很多部落格上的一句總結(其實我也是真不太懂):Node中的Event Loop是基於libuv實現的:libuvNode 的新跨平臺抽象層,libuv使用非同步,事件驅動的程式設計方式,核心是提供i/o的事件迴圈和非同步回撥。libuvAPI包含有時間,非阻塞的網路,非同步檔案操作,子程式等等。

1、Event Loop的6階段

在這裡插入圖片描述

  Node的Event loop一共分為6個階段,每個細節具體如下:

  • timers: 執行setTimeout和setInterval中到期的callback。
  • pending callback: 上一輪迴圈中少數的callback會放在這一階段執行。
  • idle, prepare: 僅在內部使用。
  • poll: 最重要的階段,執行pending callback,在適當的情況下回阻塞在這個階段。
  • check: 執行setImmediate的callback。
  • close callbacks: 執行close事件的callback,例如socket.on('close'[,fn])或者http.server.on('close, fn)。
    注意:上面六個階段都不包括 process.nextTick()
    在這裡插入圖片描述

重點:如上圖所,在Node.js中,一次巨集任務可以認為是包含上述6個階段、微任務microtask會在事件迴圈的各個階段之間執行,也就是一個階段執行完畢,就會去執行microtask佇列的任務。

2、process.nextTick()

  在第二節中就瞭解到,process.nextTick()屬於微任務,但是這裡需要重點提及下:

  • process.nextTick()雖然它是非同步API的一部分,但未在圖中顯示。因為process.nextTick()從技術上講,它不是事件迴圈的一部分;
  • 當每個階段完成後,如果存在 nextTick,就會清空佇列中的所有回撥函式,並且優先於其他 microtask 執行(可以理解為微任務中優先順序最高的

3、例項分析

  老規矩,線上程式碼:

console.log('1');

setTimeout(function() {
    console.log('2');
    process.nextTick(function() {
        console.log('3');
    })
    new Promise(function(resolve) {
        console.log('4');
        resolve();
    }).then(function() {
        console.log('5')
    })
})
process.nextTick(function() {
    console.log('6');
})
new Promise(function(resolve) {
    console.log('7');
    resolve();
}).then(function() {
    console.log('8')
})

setTimeout(function() {
    console.log('9');
    process.nextTick(function() {
        console.log('10');
    })
    new Promise(function(resolve) {
        console.log('11');
        resolve();
    }).then(function() {
        console.log('12')
    })
})
console.log('13')

將程式碼的執行分割槽進行解釋
在這裡插入圖片描述
分析:如下圖草稿所示,左上角標a為巨集任務佇列,左上角標i為微任務佇列左上角標t為timers階段佇列左上角標p為nextTick佇列同一層迴圈中,本層巨集任務先執行,再執行微任務;本層巨集任務中的非微任務非同步程式碼塊作為下層迴圈的巨集任務進入下次迴圈,如此迴圈執行:在這裡插入圖片描述

  • 1、整體程式碼可以看做巨集任務,同步程式碼直接進入主執行緒執行,輸出1,7,13,接著執行同層微任務且nextTick優先執行輸出6,8
  • 2、二層中巨集任務中只存在setTimeout,兩個setTimeout程式碼塊依次進入6階段中的timer階段t1、t2進入佇列;程式碼等價於:
setTimeout(function() {
    console.log('2');
    process.nextTick(function() {
        console.log('3');
    })
    new Promise(function(resolve) {
        console.log('4');
        resolve();
    }).then(function() {
        console.log('5')
    })
})

setTimeout(function() {
    console.log('9');
    process.nextTick(function() {
        console.log('10');
    })
    new Promise(function(resolve) {
        console.log('11');
        resolve();
    }).then(function() {
        console.log('12')
    })
})
  • 3、setTimeout中的同步程式碼立即執行輸出2,4,9,11nextTickPormise.then進入微任務執行輸出3,10,5,12
  • 4、二層中不存在6階段中的其他階段,迴圈完畢,最終輸出結果為:1->7->13->6->8->2->4->9->11->3->10->5->12

4、當堂小考

console.log('1');

setTimeout(function() {
    console.log('2');
    process.nextTick(function() {
        console.log('3');
    })
    new Promise(function(resolve) {
        console.log('4');
        resolve();
    }).then(function() {
        console.log('5')
        setTimeout(function() {
          console.log('6');
          process.nextTick(function() {
              console.log('7');
          })
          new Promise(function(resolve) {
              console.log('8');
              resolve();
          }).then(function() {
              console.log('9')
          })
      })
    })
})
process.nextTick(function() {
    console.log('10');
})
new Promise(function(resolve) {
    console.log('11');
    resolve();
}).then(function() {
    console.log('12')
    setTimeout(function() {
      console.log('13');
      process.nextTick(function() {
          console.log('14');
      })
      new Promise(function(resolve) {
          console.log('15');
          resolve();
      }).then(function() {
          console.log('16')
      })
  })
})

setTimeout(function() {
    console.log('17');
    process.nextTick(function() {
        console.log('18');
    })
    new Promise(function(resolve) {
        console.log('19');
        resolve();
    }).then(function() {
        console.log('20')
    })
})
console.log('21')

五、總結

  瀏覽器Node環境下,microtask 任務佇列的執行時機不同:Node 端,microtask 在事件迴圈的各個階段之間執行;瀏覽器端,microtask 在事件迴圈的 macrotask 執行完之後執行;

參考借鑑

相關文章