JavaScript運行機制

前言

本文要講的是,瀏覽器讀一個script代碼的流程是什麼,遇到異步代碼會如何處理,宏觀任務和微觀任務如何處理。javascript

開始前先來看幾個概念。html

棧(後進先出)

首先要說一個棧模型,函數的調用造成了棧幀。java

function f1() {     
    f2();
}
function f2() {}
f1();
複製代碼

例如這段代碼,調用 f1 時,建立第一幀;f1 調用 f2 時,建立第二幀。第二幀壓在第一幀之上,當 f2 運行完成,此時最上層第二幀彈出棧,當 f1 運行完成,此時最上層第一幀也彈出棧,棧就空了。也就是常說的後進先出。web

這個棧也就是常說的 執行棧,執行的是任務隊列裏的任務。api

隊列(先進先出)

而後說一下隊列。隊列中放着任務,也就是函數。promise

若是有新的任務(例如用戶觸發了點擊事件),會加入隊列,排在後面。瀏覽器

隊列裏的任務會放在執行棧中執行。app

隊列有兩種:宏觀隊列和微觀隊列。
分別放着兩種任務:宏觀任務和微觀任務。webapp

宏觀任務(MacroTask, 或者叫Task)

宿主發起的任務異步

如 包含代碼的script, setTimeout, setInterval, setImmediate, requestAnimationFrame, I/O, UI rendering

微觀任務(MicroTask)

JavaScript引擎發起的任務爲微觀任務

如 process.nextTick, Promises, Object.observe, MutationObserver

瀏覽器中的事件循環(Event Loop)

主線程從"任務隊列"中讀取事件,這個過程是循環不斷的,因此整個的這種運行機制又稱爲Event Loop(事件循環)。

執行棧(stack)執行過程當中若是遇到異步代碼,會移除本次執行,等處理完成後加入對應的任務列表。例如setTime延遲結束後會加入宏觀隊列,promise執行完成後會加入微觀隊列。至因而在event loop開始前判斷是否加入隊列,仍是另外開了一個線程去執行完成後加入主線程隊列,這裏不作研究。

事件循環,宏任務,微任務的關係如圖所示:

script標籤的整塊代碼是執行棧的第一個執行任務。

下面來看一段代碼

測試:宏任務微任務執行順序

setTimeout(() => console.log(1))

console.log(2);

new Promise((resolve, reject) => {
    console.log(3)
    resolve();
}).then(() => console.log(4))

console.log(5)
複製代碼

執行過程:

  • 執行第一個宏任務(整塊代碼)
  • 遇到setTimeout,跳過(到時後加入宏觀任務隊列)
  • 打印 2
  • new promise屬於同步代碼,打印 3,then屬於異步,跳過promise.then(完成後加入微觀任務隊列)
  • 打印 5
  • 執行結束
  • 有可執行的微任務,開始微任務
  • 打印4
  • 執行結束
  • 沒有可執行的微任務,開始宏任務
  • 打印1
  • 執行結束
  • 沒有微任務,沒有宏任務。

因此執行結果:2 3 5 4 1

測試:setTimeout第二個參數是最小延遲時間

var t = +new Date();

setTimeout(() => {
    console.log('timer 2秒,實際時間爲3秒');
}, 2000)

setTimeout(() => {
    console.log('timer 1秒,實際時間爲3秒');
}, 1000)

while (+new Date() - t < 3000) {} // 延遲3秒
複製代碼

解析:第一秒時候,第二個setTimeout插入宏觀任務隊列;第二秒時,第一個setTimeout插入宏觀任務隊列;第三秒

  • 第一秒:第二個setTimeout插入宏觀任務隊列;
  • 第二秒:第一個setTimeout插入宏觀任務隊列;
  • 第三秒:代碼執行結束;
  • 沒有可執行微觀任務,執行宏觀任務 輸出 'timer 1秒,實際時間爲3秒',代碼執行結束;
  • 沒有可執行微觀任務,執行宏觀任務 輸出 'timer 2秒,實際時間爲3秒',代碼執行結束;

可見setTimeout延遲結束後當即插入宏觀任務隊列。

因此執行結果爲:
timer 1秒,實際時間爲3秒
timer 2秒,實際時間爲3秒

測試:宏任務在微任務以前完成

setTimeout(() => console.log(1))

fetch('https://deployment.whosmeya.com/api/getok')
    .then(() => console.log(2))

var t = +new Date();
while (+new Date() - t < 2000) {} // 延遲兩秒
複製代碼

解析:雖然宏觀任務setTimeout在微觀任務promise以前完成,第一次宏任務(整塊代碼)執行結束後,監測到有可執行微觀任務,因此先執行微觀任務。

因此執行結果:2 1

測試:宏任務包含微任務

setTimeout(() => console.log(1))

new Promise(function (resolve, reject) {
    setTimeout(() => resolve())
}).then(() => console.log(2))
複製代碼

解析:第一個宏任務(代碼塊)執行完成後,宏觀任務隊列會有兩個宏觀任務,因此第一個setTime先執行。 執行棧的任務執行順序爲:代碼塊,第一個setTimeout,第二個setTimeout,promise.then。

因此執行結果爲:1 2

總結

event loop循環步驟:

  1. 宏任務或微任務放入執行棧執行;
  2. 若是遇到異步代碼移除本次執行(等處理完成後加入對應的任務列表);
  3. 任務執行完成(此時執行棧爲空);
  4. 若是有可執行微任務,用微任務啓動第一步,結束;
  5. 若是有可執行宏任務,用宏任務啓動第一步,結束;
  6. 重複4 5。

開始循環時,javascript標籤裏的總體代碼是第一個任務。

參考

相關文章
相關標籤/搜索