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

2022-01-21 前端進階之旅
前言

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全部代碼、setTimeout、setInterval、setImmediate(瀏覽器暫時不支持,只有IE10支持,具體可見MDN)、I/O、UI Rendering。MicroTask(微任務)Process.nextTick(Node獨有)、Promise、Object.observe(廢棄)、MutationObserver(具體使用方式查看這裡)瀏覽器中的Event Loop

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

JS調用棧

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

同步任務和異步任務

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

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

事件循環的進程模型選擇當前要執行的任務隊列,選擇任務隊列中最先進入的任務,如果任務隊列為空即null,則執行跳轉到微任務(MicroTask)的執行步驟。microtasks步驟:進入microtask檢查點。執行進入microtask檢查點時,用戶代理會執行以下步驟:當事件循環microtask執行不為空時:選擇一個最先進入的microtask隊列的microtask,將事件循環的microtask設置為已選擇的microtask,運行microtask,將已經執行完成的microtask為null,移出microtask中的microtask。設置進入microtask檢查點的標誌為false。

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

img

執行棧在執行完同步任務後,查看執行棧是否為空,如果執行棧為空,就會去檢查微任務(microTask)隊列是否為空,如果為空的話,就執行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');
複製代碼

首先我們劃分幾個分類:

第一次執行:
Tasks:run script、 setTimeout callback

Microtasks:Promise then 

JS stack: script 
Log: script start、script end。
複製代碼

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

第二次執行:
Tasks:run script、 setTimeout callback

Microtasks:Promise2 then 

JS stack: Promise2 callback 
Log: script start、script end、promise1、promise2
複製代碼

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

第三次執行:
Tasks:setTimeout callback

Microtasks: 

JS stack: setTimeout callback
Log: script start、script end、promise1、promise2、setTimeout
複製代碼

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

第四次執行:
Tasks:setTimeout callback

Microtasks: 

JS stack: 
Log: script start、script end、promise1、promise2、setTimeout
複製代碼

清空Tasks隊列和JS stack。

再舉個例子
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版本的區別在老版本版本以下,先執行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,也被稱做一個「時鐘滴答」、時標。)。引用賀老師知乎上的一個例子
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後面不會立即調用。繼續執行同步代碼,列印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 上恢復異步函數,暫停執行該函數,然後返回給調用者。

具體詳情查看(這裡)。

NodeJS的Event Loop

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

Node的Event loop一共分為6個階段,每個細節具體如下:timers: 執行setTimeout和setInterval中到期的callback。pending callback: 上一輪循環中少數的callback會放在這一階段執行。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的調度可能會延時,達不到預期時間。以下是官網文檔解釋的例子:

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階段有兩個主要功能:

當事件循環進入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
promise1
timer2
promise2

複製代碼

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

start
end
promise3
timer1
timer2
promise1
promise2
複製代碼

start
end
promise3
timer1
promise1
timer2
promise2
複製代碼

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

從下圖可能更好理解:

imgsetImmediate() 的setTimeout()的區別

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

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。

   ┌───────────────────────────┐
┌─>│           timers          │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     pending callbacks     │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare       │
│  └─────────────┬─────────────┘      ┌───────────────┐
│  ┌─────────────┴─────────────┐      │   incoming:   │
│  │           poll            │<─────┤  connections, │
│  └─────────────┬─────────────┘      │   data, etc.  │
│  ┌─────────────┴─────────────┐      └───────────────┘
│  │           check           │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
└──┤      close callbacks      │
   └───────────────────────────┘
複製代碼

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。

來源 :https://juejin.cn/post/6844903764202094606

相關焦點

  • 一次弄懂Event Loop
    為啥要弄懂Event Loop是要增加自己技術的深度,也就是懂得JavaScript的運行機制。現在在前端領域各種技術層出不窮,掌握底層原理,可以讓自己以不變,應萬變。應對各大網際網路公司的面試,懂其原理,題目任其發揮。
  • 如何解釋Event Loop面試官才滿意?
    有人說,那HTML5的新特性Web Worker,可以創建多線程呀~是的,為了解決不可避免的耗時操作(多重循環、複雜的運算),HTML5提出了Web Worker,它會在當前的js執行主線程中開闢出一個額外的線程來運行js文件,這個新的線程和js主線程之間不會互相影響,同時提供了數據交換的接口:postMessage和onMessage。
  • Event Loop淺談
    event loop 即事件循環。最初了解到js的event loop機制是通過自己對js中異步、同步的疑惑。
  • Event Loop的規範和實現
    一直以來,我對Event Loop的認知界定都是可知可不知的分級,因此僅僅保留淺顯的概念,從未真正學習過,直到看了這篇文章——《這一次,徹底弄懂
  • 深入理解 Event Loop
    本文就瀏覽器與nodejs環境下異步實現與event loop進行相關解釋。瀏覽器環境瀏覽器環境下,會維護一個任務隊列,當異步任務到達的時候加入隊列,等待事件循環到合適的時機執行。比較具體的是這樣:event-loop startmicroTasks 隊列開始清空(執行)檢查 Tasks 是否清空,有則跳到 4,無則跳到 6從 Tasks 隊列抽取一個任務,執行檢查 microTasks 是否清空,若有則跳到 2,無則跳到 3結束 event-loop也就是說,microTasks
  • jsliang 求職系列 - 06 - Event Loop
    瀏覽器五 兩個環境 Event Loop 對比六 題目訓練 6.1 同步任務 6.2 定時器 6.3 定時器 + Promise 6.4 綜合七 參考文獻 7.1 requestAnimationFrame 參考文獻 7.2 Web Worker 參考文獻 7.3 其他參考文獻二 前言 Event Loop 即事件循環,是指瀏覽器或 Node 的一種解決
  • 你不知道的 Event Loop
    In computer science, the event loop is a programming construct or design pattern that waits for and dispatches events or messages in a program.
  • 一篇文章教會你 Event loop——瀏覽器和 Node
    意義在實際工作中,了解Event loop的意義能幫助你分析一些異步次序的問題(當然,隨著es7 async和await的流行,這樣的機會越來越少了)。除此以外,它還對你了解瀏覽器和Node的內部機制有積極的作用;對於參加面試,被問到一堆異步操作的執行順序時,也不至於兩眼抓瞎。3.
  • 【小心得】淺析Nodejs Event Loop
    每一個階段都有一個裝有callbacks的fifo queue(隊列),當event loop運行到一個指定階段時,node將執行該階段的fifo queue(隊列),當隊列callback執行完或者執行callbacks數量超過該階段的上限時,event loop會轉入下一下階段.
  • EventLoop面試必考,你完全會了麼?
    它是由系統來決定回調函數的執行時機的,會請求瀏覽器在下一次重新渲染之前執行回調函數。無論設備的刷新率是多少,requestAnimationFrame 的時間間隔都會緊跟屏幕刷新一次所需要的時間;例如某一設備的刷新率是 75 Hz,那這時的時間間隔就是 13.3 ms(1 秒 / 75 次)。
  • 面試官:什麼是 EventLoop.你:一臉蒙蔽.看完這篇文章就懂了
    面試官:什麼是 EventLoop。你:一臉蒙蔽。看完這篇文章就懂了文章翻譯自:https://javascript.info/event-loop在這片文章,我們要帶著兩個問題去學習事件循環瀏覽器 js 以及
  • 徹底吃透 JavaScript 執行機制
    既然 JavaScript 是單線程語言,那麼就會存在一個問題,所有的代碼都得一句一句的來執行。就像我們在食堂排隊打飯,必須一個一個排隊點菜結帳。那些沒有排到的,就得等著~我 bug 還沒有解決掉呢?你等會。。。。其實這個時候你的一小時化妝還是 5 分鐘化妝都已經毫無意義了。。。因為哥哥這會沒空~~如果我 bug 在半個小時就解決完了,沒別的任務需要執行了,那麼就在這等著呀!必須等著!隨時待命!。然後女朋友來電話了,我化完妝了,我們出去吃飯吧,那麼剛好,我們在你的完成了請求或者 timeout 時間到了後我剛好閒著,那麼我必須立即執行了。
  • EventLoop 系列 - 聊聊 Node.js 中的事件循環
    下圖展示了它的組成部分,Network I/O 是網絡處理相關的部分,右側還有文件操作、DNS,底部 epoll、kqueue、event ports、IOCP 這些是底層不同作業系統的實現。timers(定時器階段)首先事件循環進入定時器階段,該階段包含兩個 API setTimeout(cb, ms)、setInterval(cb, ms) 前一個是僅執行一次,後一個是重複執行。
  • 【THE LAST TIME】徹底吃透 JavaScript 執行機制
    既然 JavaScript 是單線程語言,那麼就會存在一個問題,所有的代碼都得一句一句的來執行。就像我們在食堂排隊打飯,必須一個一個排隊點菜結帳。那些沒有排到的,就得等著~我 bug 還沒有解決掉呢?你等會。。。。其實這個時候你的一小時化妝還是 5 分鐘化妝都已經毫無意義了。。。因為哥哥這會沒空~~如果我 bug 在半個小時就解決完了,沒別的任務需要執行了,那麼就在這等著呀!必須等著!隨時待命!。然後女朋友來電話了,我化完妝了,我們出去吃飯吧,那麼剛好,我們在你的完成了請求或者 timeout 時間到了後我剛好閒著,那麼我必須立即執行了。
  • Android中Handler問題匯總【面試必備】
    地址 |  juejin.im/post/5e886bc3e51d4546fc795793Handler機制幾乎是Android面試時必問的問題
  • 【第1790期】圖解Event Loop
    一般情況下這並沒有什麼問題,但是假如我們要運行一個耗時30秒的任務,我們就得等待30秒後才能執行下一個任務(這30秒期間,JavaScript佔用了主線程,我們什麼都不能做,包括頁面也是卡死狀態)。這都9012年了,不帶這麼坑爹的吧?好在瀏覽器向我們提供了JS引擎不具備的特性:Web API。
  • 15 個常見的 Node.js 面試問題及答案
    對於成功的編程面試來說,準備和知識面一樣重要。準備使你有信心參加面試,而不用擔心莫名的緊張情緒。如果第一次參加編程面試,這一點尤其重要。為幫助 Node.js 開發人員更好的面試,我列出了 15 個常見的 Node.js 和網絡開發相關的面試問題。在本文中,我們將重點討論 Node.js 相關問題。
  • 瀏覽器和 Node.js 的 EventLoop 為什麼這麼設計?
    這就是瀏覽器裡的 Event Loop 的設計:設計 Loop 機制和 Task 隊列是為了支持異步,解決邏輯執行阻塞主線程的問題,設計 MicroTask 隊列的插隊機制是為了解決高優任務儘早執行的問題。
  • 這一次徹底弄懂React Router路由原理
    不過大部分同學可能只停留在會用的層面,並沒有去探究背後的原理,導致面試一被問到路由相關問題就發怵,特別是大廠面試:不懂原理,根本答不上來這些問題,也就與心儀的offer無緣了。特別是想要進大廠的同學,React相關技術棧是必須掌握的。
  • 深入解析 EventLoop 和瀏覽器渲染、幀動畫、空閒回調的關係
    進入更新渲染階段,判斷是否需要渲染,這裡有一個 rendering opportunity 的概念,也就是說不一定每一輪 event loop 都會對應一次瀏覽 器渲染,要根據屏幕刷新率、頁面性能、頁面是否在後臺運行來共同決定,通常來說這個渲染間隔是固定的。