JS 總結之事件循環

js

衆所周知,JavaScript 爲了避免複雜,被設計成了單線程。

⛅️ 任務

單線程意味着所有任務都需要按順序執行,如果某個任務執行非常耗時,線程就會被阻斷,後面的任務需要等上一個任務執行完畢纔會進行。而大多數非常耗時的任務是網絡請求,CPU 是閒着的,所以爲了資源的充分運用,便有了異步的概念。

異步便是把這些非常耗時的任務放到一邊,其他任務先進行,等處理完其它不需要等待的任務再回頭來計算剛剛被放一邊的任務。這樣就不會阻斷線程啦。

就像上面講述的,後面的任務需要等上一個任務執行完畢纔會進行,叫同步任務;把這些非常耗時的任務放到一邊,其他任務先進行,叫異步任務

那麼問題來了,執行異步任務後會發生什麼

🌦 任務隊列

在 stack 之外存在一個任務隊列

當異步任務執行完成後,會將一個回調函數(回調函數是在編寫異步任務時指定的,用來處理異步的結果)推入任務隊列,這些回調函數根據類放入到 tasksmicrotasks 中,最先被推入的函數先被推入 stack 執行,是先進先出的數據結構。由於有定時器這類功能, stack 一般要檢查時間後,某些任務纔會被執行。

🌧 事件循環

一旦 stack 沒任務了,JavaScript 引擎就會去讀取任務隊列,這個過程會循環不斷,被叫做事件循環。

🌩 setTimeout、setInterval

上文講的定時功能,依靠 setTimeout、setInterval 提供的定時功能,區別在於 setTimeout 在指定時間後執行一次,而 setInterval 則重複執行。

setTimeout 在任務隊列尾部添加了一個事件,在設定的時間後執行。但實際沒有這麼理想,當任務隊列前面的任務非常耗時,回調函數不一定在設置的時間運行。

所以常見的寫法 setTimeout(fn, 0),是指定某個任務在 stack 最早可得的空閒時間執行,也就是說,儘可能早得執行。

(注意:HTML5 標準規定了 setTimeout 的第二個參數的最小值(最短間隔),不得低於 4 毫秒,如果低於這個值,就會自動增加。)

⛈ task 與 microtask

先看一個例子:

console.log(1)

setTimeout(() => {
  console.log(2)
}, 0)

Promise.resolve()
  .then(() => {
    console.log(3)
  })
  .then(() => {
    console.log(4)
  })

console.log(5)

打印出來爲:1,5,3,4,2。why? ☃️

🌱 初探

從上文知道,每個線程都有自己的事件循環,都是獨立運行的。事件循環裏面有 task 隊列 和 mircotask 隊列,隊列裏面都按順序存放着不同的待執行任務,這些任務從不同源劃分的。

tasks 包含生成 dom 對象、解析 HTML、執行主線程 js 代碼、更改當前 URL 還有其他的一些事件如頁面加載、輸入、網絡事件和定時器事件。從瀏覽器的角度來看,tasks 代表一些離散的獨立的工作。當執行完一個 task 後,瀏覽器可以繼續其他的工作如頁面重渲染和垃圾回收。

microtasks 則是完成一些更新應用程序狀態的較小任務,如處理 promise 的回調和 DOM 的修改,這些任務在瀏覽器重渲染前執行。Microtask 應該以異步的方式儘快執行,其開銷比執行一個新的 macrotask 要小。Microtasks 使得我們可以在 UI 重渲染之前執行某些任務,從而避免了不必要的 UI 渲染,這些渲染可能導致顯示的應用程序狀態不一致。

事件循環持續不斷運行,按順序執行 task 隊列,如例子中的 setTimeout, 在 tasks 之間,瀏覽器可以更新渲染。只要 stack 爲空,mircotask 隊列就會處理,或者在每個 task 的末尾處理。在處理 mircotask 隊列期間,新添加的 microtask 添加到隊列的末尾並且也會被執行,如上文的 Promise then callback。

大概順序就是:

第一輪:檢查 task 隊列 -> 檢查 microtask 隊列 -> 檢查是否需要渲染更新
下 1 至 n 輪:...

☘ 源

一般來說,task 和 microtask 都有哪些:

task:

  • DOM 操作任務:以非阻塞方式插入文檔
  • 用戶交互任務:鼠標鍵盤事件、用戶輸入事件
  • 網絡任務
  • IndexDB 數據庫操作等 I/O
  • setTimeout / setInterval
  • history.back
  • setImmediate(涉及 node,不在這裏討論,但歸納在這)

microtask:

  • Promise.then
  • MutationObserver
  • Object.observe
  • process.nextTick(涉及 node,不在這裏討論,但歸納在這)
Jake Archibald 大大 說:setImmediate is task-queuing, whereas nextTick is before other pending work such as I/O, so it's closer to microtasks.

🍃 小試牛刀

嘗試分析一下上面的例子:

  • Promise then 的回調被分到了 promises 隊列中
  • 當打印完 5 後,當前 script 已經執行完畢,開始按順序執行 promises 隊列中的回調,打印了 3
  • 接着遇到了下一個 Promise then 的回調,也會被執行,打印 4,至此,promises 隊列已空,開始下一輪 task
  • 執行下一個 task,打印 2

所以打印了 1,5,3,4,2

🍀 運行時機

Tasks 按照順序執行,瀏覽器可能在它們的間隔渲染視圖。

Microtasks 也是按順序執行的,執行的順序,在下面兩種情況下執行:

1. 在 task 執行完之後執行。

來看一個例子:

var outer = document.querySelector('.outer')
var inner = document.querySelector('.inner')

function onClick() {
  console.log('click')

  setTimeout(function() {
    console.log('timeout')
  }, 0)

  Promise.resolve().then(function() {
    console.log('promise')
  })
}

inner.addEventListener('click', onClick)
outer.addEventListener('click', onClick)

運行結果

Edit 8l70wz1ow0

截圖

microtasks

當點擊 inner 後,console 打印:click,promise,click,promise,timeout,timeout。

執行過程:(用文字描述看不清楚,畫了個圖來一步一步根據)

觸發 inner 點擊之後:

loop1

觸發 outer 點擊之後:

loop2

2. 當 stack 爲空的時候,便執行完 microtask 隊列裏面的任務。

可以在規範 html 規範: Cleaning up after a callback step 中找到:

If the JavaScript execution context stack is now empty, perform a microtask checkpoint.

我們把上面的例子改一下:

var outer = document.querySelector('.outer')
var inner = document.querySelector('.inner')

function onClick() {
  console.log('click')

  setTimeout(function() {
    console.log('timeout')
  }, 0)

  Promise.resolve().then(function() {
    console.log('promise')
  })
}

inner.addEventListener('click', onClick)
outer.addEventListener('click', onClick)

inner.click()

加上 inner.click() 這句,情況變得不一樣。

運行結果

Edit loop2

截圖

microtasks2

當點擊 inner 後,console 打印:click,click,promise,promise,timeout,timeout。

執行過程:(還是畫圖)

觸發 inner 點擊之後:

loop3

觸發 outer 點擊之後:

loop4

這個例子與上一個不同,當執行完第 6 步,並沒有檢查 microtask 隊列,因爲 stack 並沒爲空,script 還在 stack 中。這也說明,上面的規則確保了 microtasks 不打斷當前代碼執行。

聯繫Tasks, microtasks, queues and schedules 文中的解釋:

... The above rule ensures microtasks don't interrupt JavaScript that's mid-execution. This means we don't process the microtask queue between listener callbacks, they're processed after both listeners.

⛅️ 總結

  1. 事件循環持續不斷運行;
  2. 事件循環包含 task 隊列和 microtask 隊列;
  3. task 隊列和 microtask 隊列都是按照隊列內順訊執行的,即先進先出;
  4. tasks 之間(執行完 microtasks 之後),瀏覽器可以更新渲染;
  5. microtasks 不會打斷當前代碼執行;
  6. 在 task 執行完之後執行,或者當 stack 爲空時,檢查 microtask 隊列並執行其中的任務;
  7. 新添加的 microtask 添加到隊列的末尾並且也會被執行;
  8. 事件循環同一時間內只執行一個任務;
  9. 任務一直執行到完成,不能被其他任務搶斷。

🚀 參考

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章