理解javascript中的事件循環(Event Loop)

背景

在研究js的異步的實現方式的時候,發現了JavaScript 中的 macrotask 和 microtask 的概念。在查閱了一番資料以後,對其中的執行機制有所瞭解,下面整理出來,但願能夠幫助更多人。javascript

先了解一下js的任務執行機制

首先,javascript是單線程的,因此只能經過異步解決性能問題(不然,若是前面一個任務阻塞了,那麼後續的任務都要等待,這種效果是沒法接受的)。js在執行代碼時存在着兩個比較重要的東西:執行棧和任務隊列,這兩個東西都是用來存儲任務的,區別在於:執行棧裏面存着的都是同步任務,也就是要按順序執行的任務;而任務隊列中存着的是一些異步任務,這些異步任務必定要等到執行棧清空後纔會執行(這句話很重要)。關於任務隊列,它還分紅兩種,一種叫做macrotask queue(姑且這麼命名,由於嚴格來講規範中只有說task,並無提到macrotask這個概念。這裏爲了容易區分,能夠理解爲macrotask=task!=microtask),另外一種叫做microtask queue。若是同時考慮node環境和瀏覽器環境的話,這兩種任務分別對應如下api:
microtasks:java

  • process.nextTick
  • promise
  • Object.observe
  • MutationObserver

macrotasks:node

  • setTimeout
  • setInterval
  • setImmediate
  • I/O
  • UI渲染
  • script標籤中的總體代碼

javascript在執行時,先從 macrotasks 隊列開始執行,取出第一個 macrotask 放入執行棧執行,在執行過程當中,若是遇到 macrotask,則將該 macrotask 放入 macrotask 隊列,繼續運行執行棧中的後續代碼。若是遇到microtask,那麼將該microtask放入microtask隊列,繼續向下運行執行棧中的後續代碼。當執行棧中的代碼所有執行完成後,從microtasks隊列中取出全部的microtask放入執行棧執行。執行完畢後,再從macrotasks 隊列取出下一個macrotask放入執行棧。而後不斷重複上述流程。這一過程也被稱做事件循環(Event Loop)。
javascript就是經過這種機制來實現異步的。主線程會暫時存儲I/O等異步操做,直接向下執行,當某個異步事件觸發時,再通知主線程執行相應的回調函數,經過這種機制,javascript避免了單線程中異步操做耗時對後續任務的影響。api

圖解事件循環流程

clipboard.png

根據圖中描述,一次事件循環的執行步驟以下:
一、從macrotask queue中取出最先的任務
二、在執行棧中執行第一步取出的任務
若是任務中存在microtask,將其壓入到microtask queue中
若是任務中存在macrotask,將其壓入到macrotask queue中
直到執行完畢
三、執行棧設置爲null
四、從macrotask queue中刪除執行過的macrotask
五、取出microtask queue中的所有任務,放入執行棧,
若是任務中存在microtask,將其壓入到microtask queue中
若是任務中存在macrotask,將其壓入到macrotask queue中
注意:這裏產生的microtask(也就是microtask產生的microtask )也會在這一步驟中執行。
直到當前microtask queue爲空,此步驟結束。
六、執行第一步的操做promise

實例驗證

咱們執行以下一段代碼,用上面的思路執行,看一下結果是否和預期的一致。瀏覽器

console.log('start')

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')
    })

按照上面的思路,咱們來理一下,預測一下執行結果,看看實際效果是不是這樣的。
執行流程:
第一輪:
一、首先這一整段js代碼做爲一個macrotask先被執行
二、遇到console.log('start'),輸出start
三、遇到setInterval,回調函數做爲macrotask壓入到macrotask queue中,
此時macrotask queue:[setInterval]
四、遇到setTimeout,回調函數做爲macrotask壓入到macrotask queue中,
此時macrotask queue:[setInterval,setTimeout1]
五、遇到Promise,而且調用了resolve方法,觸發了回調,回調做爲microtask壓入到microtask queue中
此時microtask queue:[promise 1,promise 2]
六、執行棧爲空,將microtask queue中的任務放入執行棧
七、執行microtask queue中Promise的回調任務,分別打印promise 1,promise 2
八、執行棧爲空,microtask queue爲空,開始下一輪事件循環
目前的console中打印內容:
start
promise 1
promise 2
目前macrotask queue:[setInterval,setTimeout1]異步

第二輪:
一、從macrotask queue中取出最先的任務,這裏對應的是第一輪中第3步的回調函數:console.log('setInterval'),輸出setInterval
二、setInterval的回調函數做爲macrotask壓入到macrotask queue中
此時macrotask queue:[setTimeout1,setInterval]
三、執行棧爲空,microtask queue爲空,開始下一輪事件循環
目前的console中打印內容:
start
promise 1
promise 2
setInterval
目前macrotask queue:[setTimeout1,setInterval]函數

第三輪:
一、從macrotask queue中取出最先的任務,目前是setTimeout1的回調,將取出的任務放入執行棧執行
二、遇到console.log('setTimeout 1'),輸出setTimeout 1
三、遇到Promise,而且調用了resolve方法,觸發回調,回調做爲microtask壓入到microtask queue中
此時microtask queue:[promise 3,promise 4,() => {setTimeout 2}]
四、執行棧爲空,將microtask queue中的任務放入執行棧
五、執行microtask queue中Promise的回調任務:
輸出promise 3
輸出promise 4
將setTimeout 2壓入macrotask queue
六、執行棧爲空,microtask queue爲空,開始下一輪事件循環
目前的console中打印內容:
start
promise 1
promise 2
setInterval
setTimeout 1
promise 3
promise 4
目前macrotask queue:[setInterval,setTimeout2]oop

第四輪:
一、從macrotask queue中取出最先的任務,這裏對應的是setInterval,輸出setInterval
二、setInterval的回調函數做爲macrotask壓入到macrotask queue中
此時macrotask queue:[setTimeout2,setInterval]
三、執行棧爲空,microtask queue爲空,開始下一輪事件循環
目前的console中打印內容:
start
promise 1
promise 2
setInterval
setTimeout 1
promise 3
promise 4
setInterval
目前macrotask queue:[setTimeout2,setInterval]性能

第五輪:
一、從macrotask queue中取出最先的任務,目前是setTimeout2的回調,將取出的任務放入執行棧執行
二、遇到console.log('setTimeout 2')輸出setTimeout 2
三、遇到Promise,而且調用了resolve方法,觸發回調,回調做爲microtask壓入到microtask queue中
此時microtask queue:[promise 5,promise 6,() => {clearInterval}]
四、執行棧爲空,將microtask queue中的任務放入執行棧
五、執行microtask queue中Promise的回調任務:
輸出promise 5
輸出promise 6
clearInterval清空setInterval計時器
六、執行棧爲空,microtask queue爲空,macrotask queue爲空,任務結束。
最終的console中打印內容:
start
promise 1
promise 2
setInterval
setTimeout 1
promise 3
promise 4
setInterval
setTimeout 2
promise 5
promise 6

經過圖片能夠看到,結果跟咱們的預期一致,在promise2的後面做爲方法的返回值,多打印了一個undefined,這個應該好理解的。

clipboard.png圖片描述

這裏面有個小問題,就是在不一樣的環境下(node/瀏覽器),promise4後面的setInterval表現可能會有差別,這裏可能跟setTimeout和setInterval的最小間隔有關,雖然咱們寫成0ms,但實際上這個最小值是有限制的,現階段不一樣組織和不一樣的js引擎實現機制存在差別,不過這個問題不在本次討論範圍以內了。若是咱們將上述代碼中setInterval的間隔設置爲10,那麼整個執行流程將嚴格符合咱們的預期。

有什麼用?

  • 後續咱們在代碼中使用Promise,setTimeout時,思路將更加清晰,用起來更佳駕輕就熟。
  • 在閱讀一些源碼時,對於一些setTimeout相關的騷操做能夠理解的更加深刻。
  • 理解javascript中的任務執行流程,加深對異步流程的理解,少犯錯誤。

總結

  • js事件循環老是從一個macrotask開始執行
  • 一個事件循環過程當中,只執行一個macrotask,可是可能執行多個microtask
  • 執行棧中的任務產生的microtask會在當前事件循環內執行
  • 執行棧中的任務產生的macrotask要在下一次事件循環纔會執行
相關文章
相關標籤/搜索