首頁  >  文章  >  web前端  >  js事件循環的實例詳解

js事件循環的實例詳解

零下一度
零下一度原創
2017-06-26 09:17:381273瀏覽

之前有看過一些事件循環的博客,不過一陣子沒看就發現自己忘光了,所以決定來自己寫一個博客總結下!

首先,我們來解釋下事件循環是什麼東西:

就我們所知,瀏覽器的js是單執行緒的,也就是說,在同一時刻,最多也只有一個程式碼段在執行,可是瀏覽器又能很好的處理非同步請求,那麼到底是為什麼呢?我們先來看一張圖
js事件循環的實例詳解
從上圖我們可以看出,js主執行緒它是有一個執行堆疊的,所有的js程式碼都會在執行堆疊裡運行。在執行程式碼過程中,如果遇到一些非同步程式碼(例如setTimeout,ajax,promise.then以及使用者點擊等操作),那麼瀏覽器就會將這些程式碼放到一個執行緒(在這裡我們叫做幕後執行緒)中去等待,不阻塞主執行緒的執行,主執行緒繼續執行堆疊中剩餘的程式碼,當幕後執行緒(background thread)裡的程式碼準備好了(比如setTimeout時間到了,ajax請求得到回應),該執行緒就會將它的回呼函數放到任務佇列中等待執行。而當主執行緒執行完堆疊中的所有程式碼後,它就會檢查任務佇列是否有任務要執行,如果有任務要執行的話,那麼就將該任務放到執行堆疊中執行。如果目前任務佇列為空的話,它就會一直循環等待任務到來。因此,這叫做事件循環。

那麼,問題來了。如果任務佇列中,有很多個任務的話,那麼要先執行哪一個任務呢?

其實(如上圖所示),js是有兩個任務佇列的,一個叫做Macrotask Queue(Task Queue),一個叫做Microtask Queue

  • #前者主要是進行一些比較大型的工作,常見的有setTimeout,setInterval,用戶交互操作,UI渲染等

  • 後者主要是進行一些比較小型的工作,常見的有Promise,process.nextTick(nodejs)

#那麼,兩者有什麼具體的區別呢?或者說,如果兩種任務同時出現的話,應該選擇哪一個呢?
其實事件循環做的事情如下:

  1. 檢查Macrotask 佇列是否為空,若不為空,則進行下一步,若為空,則跳到3

  2. 從Macrotask佇列中取隊首(在佇列時間最長)的任務進去執行堆疊中執行(僅僅一個),執行完後進入下一步

  3. 檢查Microtask佇列是否為空,若不為空,則進入下一步,否則,跳到1(開始新的事件循環)

  4. 從Microtask佇列中取隊首(在佇列時間最長)的任務進去事件佇列執行,執行完後,跳到3

其中,在執行程式碼過程中新增的microtask任務會在目前事件循環週期內執行,而新增的macrotask任務只能等到下一個事件循環才能執行了(一個事件循環只執行一個macrotask)
首先,我們先來看一段程式碼

console.log(1)
setTimeout(function() {
  //settimeout1
  console.log(2)
}, 0);
const intervalId = setInterval(function() {
  //setinterval1
  console.log(3)
}, 0)
setTimeout(function() {
  //settimeout2
  console.log(10)
  new Promise(function(resolve) {
    //promise1
    console.log(11)
    resolve()
  })
  .then(function() {
    console.log(12)
  })
  .then(function() {
    console.log(13)
    clearInterval(intervalId)
  })
}, 0);

//promise2
Promise.resolve()
  .then(function() {
    console.log(7)
  })
  .then(function() {
    console.log(8)
  })
console.log(9)

你覺得結果應該是什麼?
我在node環境和chrome控制台輸出的結果如下:

1
9
7
8
2
3
10
11
12
13

在上面的例子中
第一次事件循環:

  1. console.log(1)被執行,輸出1

  2. #settimeout1執行,加入macrotask佇列

  3. #setinterval1執行,加入macrotask佇列

  4. settimeout2執行,加入macrotask佇列

  5. promise2執行,它的兩個then函數加入microtask佇列

  6. console.log(9)執行,輸出9

  7. 根據事件循環的定義,接下來會執行新增的microtask任務,依照進入佇列的順序,執行console.log(7)和console.log(8),輸出7和8
    microtask佇列為空,回到第一步,進入下一個事件循環,此時macrotask佇列為: settimeout1,setinterval1,settimeout2

#第二次事件循環:

  1. ##從macrotask佇列取位於隊首的任務(settimeout1)並執行,輸出2

    microtask佇列為空,回到第一步,進入下一個事件循環,此時macrotask佇列為: setinterval1,settimeout2

#第三次事件循環:

  1. 從macrotask佇列裡取位於隊首的任務(setinterval1)並執行,輸出3,然後又將新生成的setinterval1加入macrotask佇列

    microtask佇列為空,回到第一步,進入下一個事件循環,此時macrotask佇列為: settimeout2,setinterval1

第四次事件循環:

  1. 從macrotask佇列裡取位於隊首的任務(settimeout2)並執行,輸出10,並且執行new Promise內的函數(new Promise內的函數是同步操作,並不是非同步操作),輸出11,並且將它的兩個then函數加入microtask佇列

  2. 從microtask佇列中,取隊首的任務執行,直到為空為止。因此,兩個新增的microtask任務依序執行,輸出12和13,並且將setinterval1清空
    此時,microtask佇列和macrotask佇列都為空,瀏覽器會一直檢查佇列是否為空,等待新的任務加入隊列。

在這裡,大家可以會想,在第一次循環中,為什麼不是macrotask先執行?因為依照流程的話,不應該是先檢查macrotask佇列是否為空,再檢查microtask佇列嗎?
原因:因為一開始js主執行緒中跑的任務就是macrotask任務,而根據事件循環的流程,一次事件循環只會執行一個macrotask任務,因此,執行完主執行緒的程式碼後,它就去從microtask佇列裡取隊首任務來執行。

注意:

由於在執行microtask任務的時候,只有當microtask佇列為空的時候,它才會進入下一個事件循環,因此,如果它源源不斷地產生新的microtask任務,就會導致主執行緒一直在執行microtask任務,而沒有辦法執行macrotask任務,這樣我們就無法進行UI渲染/IO操作/ajax請求了,因此,我們應該避免這種情況發生。在nodejs裡的process.nexttick裡,就可以設定最大的呼叫次數,以防止阻塞主執行緒。

以此,我們來引入一個新的問題,計時器的問題。定時器是否是真實可靠的呢?例如我執行一個指令:setTimeout(task, 100),他是否就能準確的在100毫秒後執行呢?其實根據以上的討論,我們就可以得知,這是不可能的。
原因我想大家應該也都知道了,因為你執行setTimeout(task,100)後,其實只是確保這個任務,會在100毫秒後進入macrotask隊列,但並不代表他能立刻運行,可能目前主執行緒正在進行一個耗時的操作,也可能目前microtask佇列有很多個任務,所以這也可能是大家一直詬病setTimeout的原因吧哈哈哈哈

以上,只是我個人對事件循環的一些看法, 以及借鑒了其他優秀文章

以上是js事件循環的實例詳解的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn