瀏覽器中的事件循環機制

瀏覽器中的事件循環機制

網上一搜事件循環, 不少文章標題的前面會加上 JavaScript, 可是我以爲事件循環機制跟 JavaScript 沒什麼關係, JavaScript 只是一門解釋型語言, 方便開發和理解的, 由V8 JIT將 JavaScript 編譯成機器語言來調用底層, 至於瀏覽器怎麼執行 JavaScript 代碼, JavaScript 管不着也不關心. 所以, 「JavaScript事件循環機制」這種說法是不合理的. 事件循環機制是由運行時環境實現的, 具體來講有瀏覽器、Node等. 這篇文章就先來講說瀏覽器中實現的事件循環機制.javascript

正文

首先,javascript 在瀏覽器端運行是單線程的,這是由瀏覽器決定的,這是爲了不多線程執行不一樣任務會發生衝突的狀況。也就是說咱們寫的javascript 代碼只在一個線程上運行,稱之爲主線程(HTML5提供了web worker API可讓瀏覽器開一個線程運行比較複雜耗時的 javascript任務,可是這個線程仍受主線程的控制)。單線程的話,若是咱們作一些「sleep」的操做好比說:html

var now = + new Date()
while (+new Date() <= now + 1000){
//這是一個耗時的操所
}

那麼在這將近一秒內,線程就會被阻塞,沒法繼續執行下面的任務。java

還有些操做好比說獲取遠程數據、I/O操做等,他們都很耗時,若是採用同步的方式,那麼進程在執行這些操做時就會由於耗時而等待,就像上面那樣,下面的任務也只能等待,這樣效率並不高。node

那瀏覽器是怎麼作的呢? web

咱們找到WHATWG規範對Event loop的介紹:算法

WHATWG Event loop定義

爲了協調事件,用戶交互,腳本,渲染,網絡等,用戶代理必須使用事件循環。chrome

事件循環的主要機制就是任務隊列機制:api

  • 一個事件循環有一個或者多個任務隊列(task queues)。任務隊列是task的有序列表,task是調度Events,Parsing,Callbacks,Using a resource,Reacting to DOM manipulation這些任務的算法;
  • 每一個任務都來自一個特定的任務源(task source)(好比鼠標鍵盤事件)。來自同一個特定任務源且屬於特定事件循環的任務必須被加入到同一個任務隊列中,來自不一樣任務源的任務能夠放在不一樣的任務隊列中;
  • 瀏覽器調用這些隊列中的任務時採起這樣的作法: 相同隊列中的任務按照先進先出的順序, 不一樣的隊列按照提早設置的隊列優先級來調用. 例如,用戶代理能夠有一個用於鼠標和鍵盤事件的任務隊列(用戶交互任務源),另外一個用於其餘任務。而後,用戶代理75%機率調用鍵盤和鼠標事件任務隊列,25%調用其餘隊列, 這樣的話就保持界面響應並且不會餓死其餘任務隊列. 可是相同隊列中的任務要按照先進先出的順序。也就是說單獨的任務隊列中的任務老是按先進先出的順序執行,可是不保證多個任務隊列中的任務優先級,具體實現可能會交叉執行

在調用任務的過程當中, 會產生新的任務, 瀏覽器就會不斷執行任務, 所以稱爲事件循環.promise

microtask queue 微任務隊列瀏覽器

還有一些特殊任務, 它們不會被放在task queues中, 會放在一個叫作microtask(微任務) queue中, 繼續看標準:

Each event loop has a microtask queue. A microtask is a task that is originally to be queued on the microtask queue rather than a task queue.

任務隊列能夠有多個, 可是微任務隊列只有一個.

那麼哪些任務是放在task queue, 哪些放在microtask queue呢? 一般對瀏覽器和Node.js來講:

  • macrotask(宏任務): script(總體代碼), setTimeout, setInterval, setImmediate, I/O, UI rendering等
  • microtask(微任務): process.nextTick, Promises(這裏指瀏覽器實現的原生 Promise), Object.observe, MutationObserver

請尤爲注意macrotask中執行總體代碼也是一個宏任務

事件循環處理過程

整體來講, 瀏覽器端事件循環的一個回合(go-around或者叫cycle)就是:

  • 從macrotask隊列中(task queue)取一個宏任務執行, 執行完後, 取出全部的microtask執行.
  • 重複回合

不管在執行macrotask仍是microtask, 都有可能產生新的macrotask或者microtask, 就這樣繼續執行.

用任務隊列機制解釋異步操做順序

這裏有一些常見異步操做:

const interval = setInterval(() => {
  console.log('setInterval')
}, 0)

setTimeout(() => {  
  console.log('setTimeout 1')
  Promise.resolve().then(() => {
    console.log('promise 3')
  }).then(() => {
    console.log('promise 4')
  }).then(() => {
    setTimeout(() => {
      console.log('setTimeout 2')
      Promise.resolve().then(() => {
        console.log('promise 5')
      }).then(() => {
        console.log('promise 6')
      }).then(() => {
          clearInterval(interval)
      })
    }, 0)
  })
}, 0)

Promise.resolve().then(() => {
  console.log('promise 1')
}).then(() => {
  console.log('promise 2')
})

結果(Chrome 63.0.3239.84; Mac OS):

promise 1
promise 2
setInterval
setTimeout 1
promise 3
promise 4
setInterval // 大部分狀況下2次, 少數狀況下一次
setTimeout 2
promise 5
promise 6

這個順序是如何得來的?

咱們先講promise 4後面只出現一次setInterval的狀況, 畫個圖簡單表示一下這個過程:

任務隊列機制

注意

本圖爲了方便把各時間段(Cycle)隊列的任務都畫在隊列中去了, 實際上執行一個task 和 microtask 後就會把這個任務從相應隊列中刪除

首先, 主任務就是執行腳本, 也就是執行上述代碼, 這也是一個task. 在執行代碼過程當中, 遇到setTimeout、setInterval 就會將回調函數添加到task queue中, 遇到 promise 就會將then回調添加到 microtask 中去.

Task執行完, 接着取全部 microtask 執行, 全部microtask 執行完了, microtask queue也就空了, 接着再取task執行, 若是microtask queue爲空, 沒有任務, 則繼續取下一個task執行, 就這樣循環執行. 圖中箭頭就表示執行的順序.

那麼爲何promise 4後面大部分狀況下出現2次setInterval, 少數狀況出現1次呢?

我猜想這是由於setInterval是有最短間隔時間的(chrome下4ms左右), 這個時間不一樣機子、不一樣瀏覽器都有可能不同. 代碼中的參數是0, 意味着儘量短的時間內就會產生一個task加入到 task queue中. 瀏覽器在執行setInterval後到執行下一個task前, 時間間隔就可能超過這個最短期, 所以會產生一個setInterval task.

我是這樣論證的:

我把含有promise五、promise6回調函數的setTimeout的時間設置大一點, 讓它推遲插入task queue中:

...  
setTimeout(() => {
      console.log('setTimeout 2')
      Promise.resolve().then(() => {
        console.log('promise 5')
      }).then(() => {
        console.log('promise 6')
      }).then(() => {
          clearInterval(interval)
      })
}, 10)   //這裏加上10ms 
...

結果是promise 4後面的setInterval出現了5次, 所以我以爲promise 4後面大部分狀況下出現2次setInterval、少數狀況出現一次的緣由就是瀏覽器在執行setInterval回調函數後、執行setTimeout回調函數前, 時間間隔大部分狀況超過了這個最短期.

另外, 我試着再依次加上1ms, 直到14ms——也就是加上4ms時, promise 4後面的setInterval變成了6次, 能夠認爲setInterval最短間隔時間在Chrome下約爲4ms(不考慮機子性能、設置).

Node中的奇怪結果

首先說明一下, 在Node中也體現了任務隊列的機制, 可是這不是Node實現的, 這是V8實現的, 由Node調用了V8任務隊列機制的API. 至於爲何是V8實現的, 咱們翻翻ECMA 262 標準對 Job 和 Job queue 的介紹就能夠得知

可是讓人摸不着頭腦的是, 這段代碼在node v8.5.0下有時會出現這樣的結果:

promise 1
promise 2
setInterval
setTimeout 1
promise 3
promise 4
setInterval
setTimeout 2
setInterval   // 爲何會出現setInterval???
promise 5
promise 6

按理說應該是setTimeout 2 => promise 5 => promise 6, 由於輸出setTimeout 2的回調函數是task, 執行完這個task後應該調用microtask 輸出promise 5 => promise 6啊? 很奇怪! Node對V8確實有些改動, 不知道是否是這方面緣由...

還請大神解惑!

你居然讀到這了

總結一下:

學習技術仍是有捷徑的, 那就是讀標準 ;)

相關文章
相關標籤/搜索