成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

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

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

[[344363]]

 為啥要弄懂 Event Loop

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

堆,棧、隊列

堆(Heap)
堆是一種數據結構,是利用完全二叉樹維護的一組數據,堆分為兩種,一種為最大堆,一種為最小堆,將根節點最大的堆叫做最大堆或大根堆,根節點最小的堆叫做最小堆或小根堆。堆是線性數據結構,相當于一維數組,有唯一后繼。

如最大堆

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

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

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

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

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

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

MacroTask(宏任務)
script 全部代碼、setTimeout、setInterval、setImmediate(瀏覽器暫時不支持,只有 IE10 支持,具體可見 MDN)、I/O、UI Rendering。

MicroTask(微任務)
Process.nextTick(Node 獨有)、Promise、Object.observe(廢棄)、MutationObserver(具體使用方式查看這里[1])

瀏覽器中的 Event Loop[2]
Javascript 有一個 main thread 主線程和 call-stack 調用棧(執行棧),所有的任務都會被放到調用棧等待主線程執行。

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

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

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

事件循環的進程模型[3]

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

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

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

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

執行棧在執行完同步任務后,查看執行棧是否為空,如果執行棧為空,就會去檢查微任務(microTask) 隊列是否為空,如果為空的話,就執行 Task(宏任務),否則就一次性執行完所有微任務。

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

舉個例子

  1. console.log('script start'); 
  2.  
  3. setTimeout(function() { 
  4. console.log('setTimeout'); 
  5. }, 0); 
  6.  
  7. Promise.resolve().then(function() { 
  8. console.log('promise1'); 
  9. }).then(function() { 
  10. console.log('promise2'); 
  11. }); 
  12. console.log('script end'); 

首先我們劃分幾個分類:

第一次執行:

  1. Tasks:run script、 setTimeout callback 
  2.  
  3. Microtasks:Promise then 
  4.  
  5. JS stack: script 
  6. Log: script start、script end。 

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

第二次執行:

  1. Tasks:run script、 setTimeout callback 
  2.  
  3. Microtasks:Promise2 then 
  4.  
  5. JS stack: Promise2 callback 
  6. Log: script start、script end、promise1、promise2 

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

第三次執行:

  1. Tasks:setTimeout callback 
  2.  
  3. Microtasks: 
  4.  
  5. JS stack: setTimeout callback 
  6. Log: script start、script end、promise1、promise2、setTimeout 

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

第四次執行:

  1. Tasks:setTimeout callback 
  2.  
  3. Microtasks: 
  4.  
  5. JS stack: 
  6. Log: script start、script end、promise1、promise2、setTimeout 

清空 Tasks 隊列和 JS stack。

以上執行幀動畫可以查看 Tasks, microtasks, queues and schedules[4]

或許這張圖也更好理解些。

再舉個例子

  1. console.log('script start'
  2.  
  3. async function async1() { 
  4. await async2() 
  5. console.log('async1 end'
  6. async function async2() { 
  7. console.log('async2 end'
  8. async1() 
  9.  
  10. setTimeout(function() { 
  11. console.log('setTimeout'
  12. }, 0) 
  13.  
  14. new Promise(resolve => { 
  15. console.log('Promise'
  16. resolve() 
  17. }) 
  18. .then(function() { 
  19. console.log('promise1'
  20. }) 
  21. .then(function() { 
  22. console.log('promise2'
  23. }) 
  24.  
  25. console.log('script end'

這里需要先理解 async/await。

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

關于 73 以下版本和 73 版本的區別
在老版本版本以下,先執行 promise1 和 promise2,再執行 async1。在 73 版本,先執行 async1 再執行promise1和 promise2。主要原因是因為在谷歌(金絲雀)73 版本中更改了規范,如下圖所示:

區別在于 RESOLVE(thenable)和之間的區別 Promise.resolve(thenable)。

在老版本中

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

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

  1. async function f() { 
  2. await p 
  3. console.log('ok'

簡化理解為:

  1. function f() { 
  2. return RESOLVE(p).then(() => { 
  3. console.log('ok'
  4. }) 
  • 如果 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 后面不會立即調用。繼續執行同步代碼,打印 Promise 和 script 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 上恢復異步函數,暫停執行該函數,然后返回給調用者。具體詳情查看(這里[5])。

NodeJS 的 Event Loop[6]

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

Node[7] 的 Event loop 一共分為 6 個階段,每個細節具體如下:

  • timers: 執行 setTimeout 和 setInterval 中到期的 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
執行 setTimeout 和 setInterval 中到期的 callback,執行這兩者回調需要設置一個毫秒數,理論上來說,應該是時間一到就立即執行 callback 回調,但是由于 system 的調度可能會延時,達不到預期時間。以下是官網文檔[8]解釋的例子:

  1. const fs = require('fs'); 
  2.  
  3. function someAsyncOperation(callback) { 
  4. // Assume this takes 95ms to complete 
  5. fs.readFile('/path/to/file', callback); 
  6.  
  7. const timeoutScheduled = Date.now(); 
  8.  
  9. setTimeout(() => { 
  10. const delay = Date.now() - timeoutScheduled; 
  11.  
  12. console.log(`${delay}ms have passed since I was scheduled`); 
  13. }, 100); 
  14.  
  15. // do someAsyncOperation which takes 95 ms to complete 
  16. someAsyncOperation(() => { 
  17. const startCallback = Date.now(); 
  18.  
  19. // do something that will take 10ms... 
  20. while (Date.now() - startCallback < 10) { 
  21. // do nothing 
  22. }); 

當進入事件循環時,它有一個空隊列(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 事件。

  1. console.log('start'
  2. setTimeout(() => { 
  3. console.log('timer1'
  4. Promise.resolve().then(function() { 
  5. console.log('promise1'
  6. }) 
  7. }, 0) 
  8. setTimeout(() => { 
  9. console.log('timer2'
  10. Promise.resolve().then(function() { 
  11. console.log('promise2'
  12. }) 
  13. }, 0) 
  14. Promise.resolve().then(function() { 
  15. console.log('promise3'
  16. }) 
  17. console.log('end'

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

  1. start 
  2. end 
  3. promise3 
  4. timer1 
  5. promise1 
  6. timer2 
  7. promise2 

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

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

  • 如果 time2 定時器已經在執行隊列中了
  1. start 
  2. end 
  3. promise3 
  4. timer1 
  5. timer2 
  6. promise1 
  7. promise2 
  • 如果 time2 定時器沒有在執行對列中,執行結果為
  1. start 
  2. end 
  3. promise3 
  4. timer1 
  5. promise1 
  6. timer2 
  7. promise2 

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

從下圖可能更好理解:

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

  • setImmediate()設計用于在當前 poll 階段完成后check階段執行腳本 。
  • setTimeout() 安排在經過最小(ms)后運行的腳本,在 timers 階段執行。舉個例子
  1. setTimeout(() => { 
  2. console.log('timeout'); 
  3. }, 0); 
  4.  
  5. setImmediate(() => { 
  6. console.log('immediate'); 
  7. }); 

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

其結果也不一致

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

  1. const fs = require('fs'); 
  2.  
  3. fs.readFile(\_\_filename, () => { 
  4. setTimeout(() => { 
  5. console.log('timeout'); 
  6. }, 0); 
  7. setImmediate(() => { 
  8. console.log('immediate'); 
  9. }); 
  10. }); 

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

然后再進入 timers 階段,執行 setTimeout,打印 timeout。

  1. ┌───────────────────────────┐ 
  2. ┌─>│ timers │ 
  3. │ └─────────────┬─────────────┘ 
  4. │ ┌─────────────┴─────────────┐ 
  5. │ │ pending callbacks │ 
  6. │ └─────────────┬─────────────┘ 
  7. │ ┌─────────────┴─────────────┐ 
  8. │ │ idle, prepare │ 
  9. │ └─────────────┬─────────────┘ ┌───────────────┐ 
  10. │ ┌─────────────┴─────────────┐ │ incoming: │ 
  11. │ │ poll │<─────┤ connections, │ 
  12. │ └─────────────┬─────────────┘ │ data, etc. │ 
  13. │ ┌─────────────┴─────────────┐ └───────────────┘ 
  14. │ │ check │ 
  15. │ └─────────────┬─────────────┘ 
  16. │ ┌─────────────┴─────────────┐ 
  17. └──┤ close callbacks │ 
  18. └───────────────────────────┘ 

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

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

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

  1. let bar; 
  2.  
  3. setTimeout(() => { 
  4. console.log('setTimeout'); 
  5. }, 0) 
  6.  
  7. setImmediate(() => { 
  8. console.log('setImmediate'); 
  9. }) 
  10. function someAsyncApiCall(callback) { 
  11. process.nextTick(callback); 
  12.  
  13. someAsyncApiCall(() => { 
  14. console.log('bar', bar); // 1 
  15. }); 
  16.  
  17. bar = 1; 

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

  1. bar 1 
  2. setTimeout 
  3. setImmediate 

另一種為:

  1. bar 1 
  2. setImmediate 
  3. setTimeout 

最后
感謝@Dante_Hu 提出這個問題 await 的問題,文章已經修正。修改了 node 端執行結果。V10 和 V11 的區別。

參考資料

 

[1]

MutationObserver: https://javascript.ruanyifeng.com/dom/mutationobserver.html

[2]

jS事件循環機制: https://segmentfault.com/a/1190000015559210

[3]

事件循環的進程模型: https://segmentfault.com/a/1190000010622146

[4]

Tasks, microtasks, queues and schedules: https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/

[5]

Promise : https://v8.js.cn/blog/fast-async/

[6]

瀏覽器與Node的事件循環(Event Loop)有何區別?: https://juejin.im/post/6844903761949753352

[7]

不要混淆nodejs和瀏覽器中的event loop: https://cnodejs.org/topic/5a9108d78d6e16e56bb80882

[8]

The Node.js Event Loop, Timers, and process.nextTick(): https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/

[9]

《又被 node 的 eventloop 坑了,這次是 node 的鍋》: https://juejin.im/post/6844903761979113479

 

 

 

責任編輯:姜華 來源: 小丑的小屋
相關推薦

2024-05-20 00:00:00

代碼主線程

2025-04-09 10:36:32

2024-10-09 12:05:27

2019-11-08 16:05:54

Promise前端鏈式調用

2019-09-12 09:40:34

秒殺系統高并發

2018-08-07 14:45:52

編程語言JavaScripthtml

2021-12-03 12:15:01

QT中文亂碼Windows

2023-02-27 08:08:54

Pulsar源碼重復消費

2009-11-27 10:31:02

GPRS路由

2010-01-11 18:05:24

VB.NET窗體繼承

2009-12-25 09:39:08

ADSL MODEM

2025-03-03 00:13:50

2010-01-04 15:05:53

2023-11-28 08:36:16

Spring中Body讀取

2009-11-24 19:50:10

2009-12-03 18:45:41

2022-10-08 23:55:58

iOS蘋果開發

2021-12-20 10:39:30

TopK排序代碼

2020-03-10 07:51:35

面試諷刺標準

2010-01-14 10:19:05

點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 亚洲午夜精品一区二区三区他趣 | 免费人成激情视频在线观看冫 | 波多野结衣一区二区三区在线观看 | 荷兰欧美一级毛片 | 欧美一区二区在线观看视频 | 婷婷桃色网 | 亚洲欧洲在线视频 | 特一级毛片 | 日韩一区二区三区视频在线观看 | 久久国产亚洲 | 欧美三级三级三级爽爽爽 | 国产高清一区二区 | 成人三级在线观看 | 在线视频一区二区三区 | 荷兰欧美一级毛片 | 日本在线综合 | 精产嫩模国品一二三区 | 精品视频一区二区三区在线观看 | 日本国产一区二区 | 国产亚洲人成a在线v网站 | a免费在线| 亚洲一区二区三区观看 | 国产精品a免费一区久久电影 | 国产乱码精品1区2区3区 | 夜夜爽99久久国产综合精品女不卡 | 欧美性生交大片免费 | 美女一区 | 亚洲黄色高清视频 | 91视频a | 国产综合久久久久久鬼色 | 久久精品中文字幕 | 精品视频在线免费观看 | 免费观看的av| 在线观看av不卡 | 天堂av资源 | 成人一区二区三区视频 | 中文字幕 亚洲一区 | 久久精品黄色 | 久久亚洲一区二区三区四区 | 拍真实国产伦偷精品 | 91久久精品 |