JS中的宏任務和微任務的區別和用法


一次弄懂Event Loop(徹底解決此類面試問題)

https://zhuanlan.zhihu.com/p/55511602

一次弄懂Event Loop(徹底解決此類面試問題)

可能存在事實錯誤

為了避免對您造成誤導,請謹慎甄別
轉載: 一次弄懂Event Loop(徹底解決此類面試問題)
作者:光光同學
出處:掘金
文章為轉載,不喜勿噴,都是前端狗,相煎何太急

前言

Event Loop即事件循環,是指瀏覽器或Node的一種解決javaScript單線程運行時不會阻塞的一種機制,也就是我們經常使用異步的原理。

為啥要弄懂Event Loop

  • 是要增加自己技術的深度,也就是懂得JavaScript的運行機制。
  • 現在在前端領域各種技術層出不窮,掌握底層原理,可以讓自己以不變,應萬變。
  • 應對各大互聯網公司的面試,懂其原理,題目任其發揮。

 

堆,棧、隊列

 

堆(Heap)

堆是一種數據結構,是利用完全二叉樹維護的一組數據,堆分為兩種,一種為最大堆,一種為最小堆,將根節點最大的堆叫做最大堆或大根堆,根節點最小的堆叫做最小堆或小根堆。

堆是線性數據結構,相當於一維數組,有唯一后繼。

如最大堆

 

 

棧(Stack)

棧在計算機科學中是限定僅在表尾進行插入或刪除操作的線性表。棧是一種數據結構,它按照后進先出的原則存儲數據,先進入的數據被壓入棧底,最后的數據在棧頂,需要讀數據的時候從棧頂開始彈出數據。

棧是只能在某一端插入和刪除的特殊線性表。

 

隊列(Queue)

特殊之處在於它只允許在表的前端(front)進行刪除操作,而在表的后端(rear)進行插入操作,和棧一樣,隊列是一種操作受限制的線性表。

進行插入操作的端稱為隊尾,進行刪除操作的端稱為隊頭。 隊列中沒有元素時,稱為空隊列。

隊列的數據元素又稱為隊列元素。在隊列中插入一個隊列元素稱為入隊,從隊列中刪除一個隊列元素稱為出隊。因為隊列只允許在一端插入,在另一端刪除,所以只有最早進入隊列的元素才能最先從隊列中刪除,故隊列又稱為先進先出(FIFO—first in first out

Event Loop

JavaScript中,任務被分為兩種,一種宏任務(MacroTask)也叫Task,一種叫微任務(MicroTask)。

MacroTask(宏任務)

  • script全部代碼、setTimeoutsetIntervalsetImmediate(瀏覽器暫時不支持,只有IE10支持,具體可見MDN)、I/OUI Rendering

 

MicroTask(微任務)

  • Process.nextTick(Node獨有)PromiseObject.observe(廢棄)MutationObserver(具體使用方式查看這里

 

瀏覽器中的Event Loop

Javascript 有一個 main thread 主線程和 call-stack 調用棧(執行棧),所有的任務都會被放到調用棧等待主線程執行。

JS調用棧

JS調用棧采用的是后進先出的規則,當函數執行的時候,會被添加到棧的頂部,當執行棧執行完成后,就會從棧頂移出,直到棧內被清空。

同步任務和異步任務

Javascript單線程任務被分為同步任務和異步任務,同步任務會在調用棧中按照順序等待主線程依次執行,異步任務會在異步任務有了結果后,將注冊的回調函數放入任務隊列中等待主線程空閑的時候(調用棧被清空),被讀取到棧內等待主線程的執行。

任務隊列Task Queue,即隊列,是一種先進先出的一種數據結構。

 

事件循環的進程模型

  • 選擇當前要執行的任務隊列,選擇任務隊列中最先進入的任務,如果任務隊列為空即null,則執行跳轉到微任務(MicroTask)的執行步驟。
  • 將事件循環中的任務設置為已選擇任務。
  • 執行任務。
  • 將事件循環中當前運行任務設置為null。
  • 將已經運行完成的任務從任務隊列中刪除。
  • microtasks步驟:進入microtask檢查點。
  • 更新界面渲染。
  • 返回第一步。

 

執行進入microtask檢查點時,用戶代理會執行以下步驟:

  • 設置microtask檢查點標志為true。
  • 當事件循環microtask執行不為空時:選擇一個最先進入的microtask隊列的microtask,將事件循環的microtask設置為已選擇的microtask,運行microtask,將已經執行完成的microtasknull,移出microtask中的microtask
  • 清理IndexDB事務
  • 設置進入microtask檢查點的標志為false。

 

上述可能不太好理解,下圖是我做的一張圖片。

執行棧在執行完同步任務后,查看執行棧是否為空,如果執行棧為空,就會去執行Task(宏任務),每次宏任務執行完畢后,檢查微任務(microTask)隊列是否為空,如果不為空的話,會按照先入先出的規則全部執行完微任務(microTask)后,設置微任務(microTask)隊列為null,然后再執行宏任務,如此循環。

舉個例子

console.log('script start'); setTimeout(function() { console.log('setTimeout'); }, 0); Promise.resolve().then(function() { console.log('promise1'); }).then(function() { console.log('promise2'); }); console.log('script end'); 

首先我們划分幾個分類:

第一次執行:

Tasksrun scriptsetTimeout callback MicrotasksPromise then JS stack: script Log: script startscript end

執行同步代碼,將宏任務(Tasks)和微任務(Microtasks)划分到各自隊列中。

第二次執行:

Tasksrun scriptsetTimeout callback MicrotasksPromise2 then JS stack: Promise2 callback Log: script startscript endpromise1promise2 

執行宏任務后,檢測到微任務(Microtasks)隊列中不為空,執行Promise1,執行完成Promise1后,調用Promise2.then,放入微任務(Microtasks)隊列中,再執行Promise2.then

第三次執行:

TaskssetTimeout callback MicrotasksJS stack: setTimeout callback Log: script startscript endpromise1promise2setTimeout 

當微任務(Microtasks)隊列中為空時,執行宏任務(Tasks),執行setTimeout callback,打印日志。

第四次執行:

TaskssetTimeout callback MicrotasksJS stack: Log: script startscript endpromise1promise2setTimeout 

清空Tasks隊列和JS stack

以上執行幀動畫可以查看Tasks, microtasks, queues and schedules
或許這張圖也更好理解些。

 

再舉個例子

console.log('script start') async function async1() { await async2() console.log('async1 end') } async function async2() { console.log('async2 end') } async1() setTimeout(function() { console.log('setTimeout') }, 0) new Promise(resolve => { console.log('Promise') resolve() }) .then(function() { console.log('promise1') }) .then(function() { console.log('promise2') }) console.log('script end') 

這里需要先理解async/await
async/await 在底層轉換成了 promise 和 then 回調函數。
也就是說,這是 promise 的語法糖。
每次我們使用 await, 解釋器都創建一個 promise 對象,然后把剩下的 async 函數中的操作放到 then 回調函數中。
async/await 的實現,離不開 Promise。從字面意思來理解,async 是“異步”的簡寫,而 await 是 async wait 的簡寫可以認為是等待異步方法執行完成。

關於73以下版本和73版本的區別

  • 在73版本以下,先執行promise1promise2,再執行async1
  • 在73版本,先執行async1再執行promise1promise2

 

主要原因是因為在谷歌(金絲雀)73版本中更改了規范,如下圖所示:

 

 

  • 區別在於RESOLVE(thenable)和之間的區別Promise.resolve(thenable)

 

在73以下版本中

  • 首先,傳遞給 await 的值被包裹在一個 Promise 中。然后,處理程序附加到這個包裝的 Promise,以便在 Promise 變為 fulfilled 后恢復該函數,並且暫停執行異步函數,一旦 promise 變為 fulfilled,恢復異步函數的執行。
  • 每個 await 引擎必須創建兩個額外的 Promise(即使右側已經是一個 Promise)並且它需要至少三個 microtask 隊列 tickstick為系統的相對時間單位,也被稱為系統的時基,來源於定時器的周期性中斷(輸出脈沖),一次中斷表示一個tick,也被稱做一個“時鍾滴答”、時標。)。

 

引用賀老師知乎上的一個例子

async function f() { await p console.log('ok') } 

簡化理解為:

function f() { return RESOLVE(p).then(() => { console.log('ok') }) } 
  • 如果 RESOLVE(p) 對於 p 為 promise 直接返回 p 的話,那么 p的 then 方法就會被馬上調用,其回調就立即進入 job 隊列。
  • 而如果 RESOLVE(p) 嚴格按照標准,應該是產生一個新的 promise,盡管該 promise確定會 resolve 為 p,但這個過程本身是異步的,也就是現在進入 job 隊列的是新 promise 的 resolve過程,所以該 promise 的 then 不會被立即調用,而要等到當前 job 隊列執行到前述 resolve 過程才會被調用,然后其回調(也就是繼續 await 之后的語句)才加入 job 隊列,所以時序上就晚了。

 

谷歌(金絲雀)73版本中

  • 使用對PromiseResolve的調用來更改await的語義,以減少在公共awaitPromise情況下的轉換次數。
  • 如果傳遞給 await 的值已經是一個 Promise,那么這種優化避免了再次創建 Promise 包裝器,在這種情況下,我們從最少三個 microtick 到只有一個 microtick

 

詳細過程:


73以下版本

  • 首先,打印script start,調用async1()時,返回一個Promise,所以打印出來async2 end
  • 每個 await,會新產生一個promise,但這個過程本身是異步的,所以該await后面不會立即調用。
  • 繼續執行同步代碼,打印Promisescript end,將then函數放入微任務隊列中等待執行。
  • 同步執行完成之后,檢查微任務隊列是否為null,然后按照先入先出規則,依次執行。
  • 然后先執行打印promise1,此時then的回調函數返回undefinde,此時又有then的鏈式調用,又放入微任務隊列中,再次打印promise2
  • 再回到await的位置執行返回的 Promise 的 resolve 函數,這又會把 resolve 丟到微任務隊列中,打印async1 end
  • 當微任務隊列為空時,執行宏任務,打印setTimeout

 

谷歌(金絲雀73版本)

  • 如果傳遞給 await 的值已經是一個 Promise,那么這種優化避免了再次創建 Promise 包裝器,在這種情況下,我們從最少三個 microtick 到只有一個 microtick
  • 引擎不再需要為 await 創造 throwaway Promise - 在絕大部分時間。
  • 現在 promise 指向了同一個 Promise,所以這個步驟什么也不需要做。然后引擎繼續像以前一樣,創建 throwaway Promise,安排 PromiseReactionJob 在 microtask 隊列的下一個 tick 上恢復異步函數,暫停執行該函數,然后返回給調用者。

具體詳情查看(這里)。

NodeJS的Event Loop

 

Node中的Event Loop是基於libuv實現的,而libuv是 Node 的新跨平台抽象層,libuv使用異步,事件驅動的編程方式,核心是提供i/o的事件循環和異步回調。libuv的API包含有時間,非阻塞的網絡,異步文件操作,子進程等等。 Event Loop就是在libuv中實現的。

NodeEvent loop一共分為6個階段,每個細節具體如下:

  • timers: 執行setTimeoutsetInterval中到期的callback
  • pending callback: 上一輪循環中少數的callback會放在這一階段執行。
  • idle, prepare: 僅在內部使用。
  • poll: 最重要的階段,執行pending callback,在適當的情況下回阻塞在這個階段。
  • check: 執行setImmediate(setImmediate()是將事件插入到事件隊列尾部,主線程和事件隊列的函數執行完成之后立即執行setImmediate指定的回調函數)的callback
  • close callbacks: 執行close事件的callback,例如socket.on('close'[,fn])或者http.server.on('close, fn)

 

具體細節如下:

timers
執行setTimeoutsetInterval中到期的callback,執行這兩者回調需要設置一個毫秒數,理論上來說,應該是時間一到就立即執行callback回調,但是由於system的調度可能會延時,達不到預期時間。
以下是官網文檔解釋的例子:

const fs = require('fs'); function someAsyncOperation(callback) { // Assume this takes 95ms to complete  fs.readFile('/path/to/file', callback); } const timeoutScheduled = Date.now(); setTimeout(() => { const delay = Date.now() - timeoutScheduled; console.log(`${delay}ms have passed since I was scheduled`); }, 100); // do someAsyncOperation which takes 95 ms to complete someAsyncOperation(() => { const startCallback = Date.now(); // do something that will take 10ms...  while (Date.now() - startCallback < 10) { // do nothing  } }); 

當進入事件循環時,它有一個空隊列(fs.readFile()尚未完成),因此定時器將等待剩余毫秒數,當到達95ms時,fs.readFile()完成讀取文件並且其完成需要10毫秒的回調被添加到輪詢隊列並執行。
當回調結束時,隊列中不再有回調,因此事件循環將看到已達到最快定時器的閾值,然后回到timers階段以執行定時器的回調。
在此示例中,您將看到正在調度的計時器與正在執行的回調之間的總延遲將為105毫秒。

以下是我測試時間:

pending callbacks


此階段執行某些系統操作(例如TCP錯誤類型)的回調。 例如,如果TCP socket ECONNREFUSED在嘗試connect時receives,則某些* nix系統希望等待報告錯誤。 這將在pending callbacks階段執行。

poll

該poll階段有兩個主要功能:

  • 執行I/O回調。
  • 處理輪詢隊列中的事件。

 

當事件循環進入poll階段並且在timers中沒有可以執行定時器時,將發生以下兩種情況之一

  • 如果poll隊列不為空,則事件循環將遍歷其同步執行它們的callback隊列,直到隊列為空,或者達到system-dependent(系統相關限制)。

如果poll隊列為空,則會發生以下兩種情況之一

  • 如果有setImmediate()回調需要執行,則會立即停止執行poll階段並進入執行check階段以執行回調。
  • 如果沒有setImmediate()回到需要執行,poll階段將等待callback被添加到隊列中,然后立即執行。

當然設定了 timer 的話且 poll 隊列為空,則會判斷是否有 timer 超時,如果有的話會回到 timer 階段執行回調。

check


此階段允許人員在poll階段完成后立即執行回調。
如果poll階段閑置並且script已排隊setImmediate(),則事件循環到達check階段執行而不是繼續等待。


setImmediate()實際上是一個特殊的計時器,它在事件循環的一個單獨階段運行。它使用libuv API來調度在poll階段完成后執行的回調。
通常,當代碼被執行時,事件循環最終將達到poll階段,它將等待傳入連接,請求等。
但是,如果已經調度了回調setImmediate(),並且輪詢階段變為空閑,則它將結束並且到達check階段,而不是等待poll事件。

console.log('start') setTimeout(() => { console.log('timer1') Promise.resolve().then(function() { console.log('promise1') }) }, 0) setTimeout(() => { console.log('timer2') Promise.resolve().then(function() { console.log('promise2') }) }, 0) Promise.resolve().then(function() { console.log('promise3') }) console.log('end') 

如果node版本為v11.x, 其結果與瀏覽器一致。

start end promise3 timer1 timer2 promise1 promise2 

具體詳情可以查看《又被node的eventloop坑了,這次是node的鍋》。

如果v10版本上述結果存在兩種情況:

  • 如果time2定時器已經在執行隊列中了,那么執行結果與上面結果相同。
  • 如果time2定時器沒有在執行對列中,執行結果為

 

start end promise3 timer1 promise1 timer2 

具體情況可以參考poll階段的兩種情況。

從下圖可能更好理解:

 

setImmediate() 的setTimeout()的區別

setImmediatesetTimeout()是相似的,但根據它們被調用的時間以不同的方式表現。

  • setImmediate()設計用於在當前poll階段完成后check階段執行腳本 。
  • setTimeout() 安排在經過最小(ms)后運行的腳本,在timers階段執行。

 

舉個例子

setTimeout(() => { console.log('timeout'); }, 0); setImmediate(() => { console.log('immediate'); }); 

執行定時器的順序將根據調用它們的上下文而有所不同。 如果從主模塊中調用兩者,那么時間將受到進程性能的限制。

其結果也不一致

如果在I / O周期內移動兩個調用,則始終首先執行立即回調:

const fs = require('fs'); fs.readFile(__filename, () => { setTimeout(() => { console.log('timeout'); }, 0); setImmediate(() => { console.log('immediate'); }); }); 

其結果可以確定一定是immediate => timeout
主要原因是在I/O階段讀取文件后,事件循環會先進入poll階段,發現有setImmediate需要執行,會立即進入check階段執行setImmediate的回調。
然后再進入timers階段,執行setTimeout,打印timeout

Process.nextTick()


process.nextTick()雖然它是異步API的一部分,但未在圖中顯示。這是因為process.nextTick()從技術上講,它不是事件循環的一部分。

  • process.nextTick()方法將 callback 添加到next tick隊列。 一旦當前事件輪詢隊列的任務全部完成,在next tick隊列中的所有callbacks會被依次調用。

 

換種理解方式:

  • 當每個階段完成后,如果存在 nextTick 隊列,就會清空隊列中的所有回調函數,並且優先於其他 microtask 執行。

 

例子

let bar; setTimeout(() => { console.log('setTimeout'); }, 0) setImmediate(() => { console.log('setImmediate'); }) function someAsyncApiCall(callback) { process.nextTick(callback); } someAsyncApiCall(() => { console.log('bar', bar); // 1 }); bar = 1; 

在NodeV10中上述代碼執行可能有兩種答案,一種為:

bar 1 setTimeout setImmediate 

另一種為:

bar 1 setImmediate setTimeout 

無論哪種,始終都是先執行process.nextTick(callback),打印bar 1


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM