深刻理解 Event Loop

衆所周知,JavaScript(如下簡稱 JS) 是單線程語言,在 html5 中增長了 web workers,web workers 是新開了線程執行的,那麼 JS 仍是單線程的嗎?固然是,爲何要設計成單線程?javascript

網上有不少說法,大部分都說是多個線程同時對一個dom操做(同時修改dom內容,一個線程增長屬性,一個線程刪除屬性),會很是混亂,固然若是支持多線程就會相應的就要加入多線程的鎖機制,那麼 JS 就變得很是複雜了,想一想 JS 最開始設計的初衷就是用於用戶交互,並且當時的原始需求是:功能不須要太強,語法較爲簡單,容易學習和部署,Brendan Eich 只用了10天,就設計完成了這種語言的初版,所以也不可能加入多線程這麼複雜的技術。html

即便如今支持 web workers,因爲沒有多線程的機制,web workers 和執行線程只能經過 postMessage 來通訊,並且因爲沒有鎖,web workers 沒法訪問 window 和 document 對象。html5

JS 的單線程是指一個瀏覽器進程中只有一個 JS 的執行線程,即同一時刻內只會有一段代碼在執行。java

Micro-Task 與 Macro-Task

單線程如何實現異步?JS 設計了一個事件循環的方式。全部的代碼執行均按照事件循環的方式進行。web

事件循環中分兩種任務:一個是宏任務(Macro-Task),另外一個是微任務(Micro-Task)。常見的宏任務和微任務以下。promise

宏任務:script(總體代碼)、setTimeout、setInterval、requestAnimationFrame、I/O、事件、MessageChannel、setImmediate (Node.js) 微任務:Promise.then、 MutaionObserver、process.nextTick (Node.js)瀏覽器

事件循環按下圖的方式進行。bash

注意: 宏任務執行完後,須要清空當前微任務隊列後纔回去執行下一個宏任務,若是微任務裏面產生了新的微任務,仍然會在當前事件循環裏面被執行完,後面會舉例說明。多線程

來個示例驗證下上面的流程。dom

<script> console.log(1); setTimeout(function timeout1() { console.log(2); }, 0); Promise.resolve().then(function promise1() { console.log(3); setTimeout(function timeout2() { console.log(4); Promise.resolve().then(function promise2() { console.log(5); }); }, 0); return Promise.resolve() .then(function promise3() { console.log(6); return Promise.resolve().then(function promise4() { console.log(7); }); }) .then(function promise5() { console.log(8); }); }) console.log(9); </script>

<script> console.log(10); setTimeout(function timeout3() { console.log(11); }, 0); Promise.resolve().then(function promise6() { console.log(12); }); </script>
複製代碼

按照上面流程梳理下執行流程:

  1. 將兩個宏任務(兩個script代碼)初始化進宏任務隊列,宏任務隊列爲:[script1, script2]
  2. script1 出隊壓入執行棧執行,宏任務隊列爲:[script2]
  3. 同步代碼執行輸出:1,
  4. 0ms 後把 timeout1 放入宏任務隊列,宏任務隊列爲:[script2, timeout1]
  5. promise1 入隊,微任務隊列爲:[promise1]
  6. 同步代碼執行輸出:9
  7. script1 執行完畢,進入微任務執行階段,promise1 出隊壓入執行棧執行,微任務隊列爲空
  8. 同步代碼執行輸出:3
  9. 0ms 後把 timeout2 放入宏任務隊列,宏任務隊列爲:[script2, timeout1, timeout2]
  10. promise3 入隊,微任務隊列爲:[promise3]
  11. promise1 執行完畢,繼續判斷微任務隊列是否爲空,promise3 出隊壓入執行棧執行,微任務隊列爲空
  12. 同步代碼執行輸出:6
  13. promise4 入隊,微任務隊列爲:[promise4]
  14. promise3 執行完畢,promise5 入隊,微任務隊列爲:[promise4,promise5]
  15. 判斷微任務隊列是否爲空,promise4 出隊壓入執行棧執行,微任務隊列爲:[promise5]
  16. 同步代碼執行輸出:7
  17. promise4 執行完畢,繼續判斷微任務隊列是否爲空,promise5 出隊壓入執行棧執行,微任務隊列爲空
  18. 同步代碼執行輸出:8
  19. 微任務隊列清空,宏任務 script2 出隊壓入執行棧執行,宏任務隊列爲空
  20. 同步代碼執行輸出:10
  21. 0ms 後把 timeout3 放入宏任務隊列,宏任務隊列爲:[timeout1, timeout2, timeout3]
  22. promise6 入隊,微任務隊列爲:[promise6]
  23. script2 執行完畢,進入微任務執行階段,promise6 出隊壓入執行棧執行,微任務隊列爲空
  24. 同步代碼執行輸出:12
  25. 微任務隊列爲空,宏任務 timeout1 壓入執行棧執行,宏任務隊列爲[timeout2, timeout3]
  26. 同步代碼執行輸出:2
  27. timeout1執行完畢,微任務隊列爲空,宏任務 timeout2 壓入執行棧執行,宏任務隊列爲[timeout3]
  28. 同步代碼執行輸出:4,promise2 入隊,微任務隊列爲:[promise2]
  29. timeout2 執行完畢,判斷微任務隊列是否爲空,promise2 出隊壓入執行棧執行,微任務隊列爲空
  30. 同步代碼執行輸出:5
  31. promise2執行完,微任務隊列爲空,宏任務 timeout2 壓入執行棧執行,宏任務隊列爲空
  32. 同步代碼執行輸出:11
  33. timeout3執行完畢,微任務隊列爲空,宏任務隊列爲空

setTimeout

setTimeout 的 delay 最小值在不一樣瀏覽器的有差別,在 Chrome 74 上測試的結果是 2ms,Firefox 67 上測試的記過是 1ms。

最小值是什麼意思?就是小於這個值後,瀏覽器按照0處理。好比在 Chrome 上,測試下面的代碼:

setTimeout(function(){console.log(1)},1.99);
setTimeout(function(){console.log(2)},0);
複製代碼

輸出的結果爲 一、2,而

setTimeout(function(){console.log(1)},2);
setTimeout(function(){console.log(2)},0);
複製代碼

輸出的結果爲 二、1,說明 2ms 是有效的。

另外 setTimeout 是從調用開始計時,到了時間就放入宏任務隊列,咱們來看下面的例子。

var s = Date.now()
setTimeout(function timeout1() {
    console.log(1)
}, 200)

while (Date.now() - s <= 200) {
}

setTimeout(function timeout2() {
    console.log(2)
}, 0)
複製代碼
  1. timeout1 200ms 後會放入到宏任務隊列中
  2. while 執行了 200ms,此時 timeout1 已經先添加到宏任務隊列中,所以最終打印結果爲:一、2
  3. 若是將 while 的時間設置小於 200ms,考慮到代碼執行須要花費時間,將 while 的條件改成Date.now() - s <= 198
  4. 測試 while 執行只花費了 198ms,timeout2 會被先添加到宏任務隊列中,所以最終打印結果會是:二、1

setInterval

和 setTimeout 相同,調用開始計時,按 delay 時間將回調添加到宏任務隊列中。那麼 setInterval 是按 delay 不斷的向宏任務隊列添加任務,仍是須要等待已添加的任務執行完後再添加,仍是其餘機制?

思考下面代碼:

var start = Date.now()

var id = setInterval(function interval() {

    var whileStart = Date.now()   
    console.log(whileStart - start)                 // 輸出 interval1 調用的時間和最開始調用計時的時間差,即過了多久才調用
    while (Date.now() - whileStart < 250) {   // 至關於 sleep 250ms
    }
}, 100)

setTimeout(function timeout() {
    clearInterval(id)
    console.log(Date.now() - start)       
}, 400)
複製代碼

打印的時間間隔是?

100
351
605
855
複製代碼

爲了更好的理解,用圖示來解釋上面的流程。

參考

JavaScript語言的歷史

相關文章
相關標籤/搜索