06.20 Js 的事件循環(Event Loop)機制以及實例講解

前言

大家都知道js是單線程的腳本語言,在同一時間,只能做同一件事,為了協調事件、用戶交互、腳本、UI渲染和網絡處理等行為,防止主線程阻塞,Event Loop方案應運而生...為什麼js是單線程?

js作為主要運行在瀏覽器的腳本語言,js主要用途之一是操作DOM。

在js高程中舉過一個栗子,如果js同時有兩個線程,同時對同一個dom進行操作,這時瀏覽器應該聽哪個線程的,如何判斷優先級?

為了避免這種問題,js必須是一門單線程語言,並且在未來這個特點也不會改變。


執行棧與任務隊列

因為js是單線程語言,當遇到異步任務(如ajax操作等)時,不可能一直等待異步完成,再繼續往下執行,在這期間瀏覽器是空閒狀態,顯而易見這會導致巨大的資源浪費。

執行棧

當執行某個函數、用戶點擊一次鼠標,Ajax完成,一個圖片加載完成等事件發生時,只要指定過回調函數,這些事件發生時就會進入執行棧隊列中,等待主線程讀取,遵循先進先出原則。

主線程

要明確的一點是,主線程跟執行棧是不同概念,主線程規定現在執行執行棧中的哪個事件。

主線程循環:即主線程會不停的從執行棧中讀取事件,會執行完所有棧中的同步代碼。

當遇到一個異步事件後,並不會一直等待異步事件返回結果,而是會將這個事件掛在與執行棧不同的隊列中,我們稱之為任務隊列(Task Queue)。

當主線程將執行棧中所有的代碼執行完之後,主線程將會去查看任務隊列是否有任務。如果有,那麼主線程會依次執行那些任務隊列中的回調函數。

不太理解的話,可以運行一下下面的代碼,或者點擊一下這個demo

結果是當a、b、c函數都執行完成之後,三個setTimeout才會依次執行。

let a = () => {
setTimeout(() => {
console.log('任務隊列函數1')
}, 0)
for (let i = 0; i < 5000; i++) {
console.log('a的for循環')
}
console.log('a事件執行完')
}
let b = () => {
setTimeout(() => {
console.log('任務隊列函數2')
}, 0)
for (let i = 0; i < 5000; i++) {

console.log('b的for循環')
}
console.log('b事件執行完')
}
let c = () => {
setTimeout(() => {
console.log('任務隊列函數3')
}, 0)
for (let i = 0; i < 5000; i++) {
console.log('c的for循環')
}
console.log('c事件執行完')
}
a();
b();
c();
// 當a、b、c函數都執行完成之後,三個setTimeout才會依次執行

js 異步執行的運行機制。

  1. 所有任務都在主線程上執行,形成一個執行棧。
  2. 主線程之外,還存在一個"任務隊列"(task queue)。只要異步任務有了運行結果,就在"任務隊列"之中放置一個事件。
  3. 一旦"執行棧"中的所有同步任務執行完畢,系統就會讀取"任務隊列"。那些對應的異步任務,結束等待狀態,進入執行棧並開始執行。
  4. 主線程不斷重複上面的第三步

宏任務與微任務:

異步任務分為 宏任務(macrotask) 與 微任務 (microtask),不同的API註冊的任務會依次進入自身對應的隊列中,然後等待 Event Loop 將它們依次壓入執行棧中執行。

宏任務(macrotask):

script(整體代碼)、setTimeout、setInterval、UI 渲染、 I/O、postMessage、 MessageChannel、setImmediate(Node.js 環境)

微任務(microtask):

Promise、 MutaionObserver、process.nextTick(Node.js環境)

Event Loop(事件循環):

Event Loop(事件循環)中,每一次循環稱為 tick, 每一次tick的任務如下:

  • 執行棧選擇最先進入隊列的宏任務(通常是script整體代碼),如果有則執行
  • 檢查是否存在 Microtask,如果存在則不停的執行,直至清空 microtask 隊列
  • 更新render(每一次事件循環,瀏覽器都可能會去更新渲染)
  • 重複以上步驟

宏任務 > 所有微任務 > 宏任務,如下圖所示:

Js 的事件循環(Event Loop)機制以及實例講解

從上圖我們可以看出:

  1. 將所有任務看成兩個隊列:執行隊列與事件隊列。
  2. 執行隊列是同步的,事件隊列是異步的,宏任務放入事件列表,微任務放入執行隊列之後,事件隊列之前。
  3. 當執行完同步代碼之後,就會執行位於執行列表之後的微任務,然後再執行事件列表中的宏任務

上面提到的demo結果可以這麼理解:先執行script宏任務,執行完了之後,再執行其他兩個定時器宏任務。


面試題實踐

下面這個題,很多人都應該看過/遇到過,重新來看會不會覺得清晰很多:

 // 執行順序問題,考察頻率挺高的,先自己想答案**
setTimeout(function () {
console.log(1);
});
new Promise(function(resolve,reject){
console.log(2)
resolve(3)
}).then(function(val){
console.log(val);

})
console.log(4);

根據本文的解析,我們可以得到:

  1. 先執行script同步代碼
 先執行new Promise中的console.log(2),then後面的不執行屬於微任務
然後執行console.log(4)
  1. 執行完script宏任務後,執行微任務,console.log(3),沒有其他微任務了。
  2. 執行另一個宏任務,定時器,console.log(1)。

根據本文的內容,可以很輕鬆,且有理有據的猜出寫出正確答案:2,4,3,1.


結語

類似上文的面試題還有很多,實則都大同小異,只要掌握了事件循環的機制,這些問題都會變得很簡單。

作者:OBKoro1 原文鏈接:https://juejin.im/post/5b24b116e51d4558a65fdb70


分享到:


相關文章: