一、事件循環基礎
由於JavaScript是一種單線程的編程語言,因此JavaScript中的所有任務都需要排隊依次完成。但這樣的設計明顯會有很大的一個問題,那就是如果碰到一個需要耗費很多的時間完成的事件時,很有可能會造成線程的阻塞問題。因此,JavaScript的開發者就將所有的任務分為兩種來解決這種問題:
① 同步任務(在主線程中只有前面的代碼執行完畢后,后面的才能執行)
② 異步任務(從主線程提出,進入任務隊列執行,等執行完畢后通知主線程,這個異步任務可以執行了,才會進入主線程執行)
如圖:
JavaScript的執行機制主要是以下三步
① 所有同步任務都在主線程上執行,形成一個執行棧(execution context stack)。
② 主線程之外,還存在一個‘任務隊列’(task queue)。只要異步任務有了運行結果,就在”任務隊列”之中放置一個事件。
③ 一旦主線程的棧中的所有同步任務執行完畢,系統就會讀取任務隊列,選擇需要首先執行的任務然后執行。
在此過程中,主線程要做的就是從任務隊列中去實踐,執行事件,執行完畢,再取事件,再執行事件…這樣不斷取事件,執行事件的循環機制就叫做事件循環機制。(需要注意的的是當任務隊列為空時,就會等待直到任務隊列變成非空。)
二、深入事件循環
console.log('script start'); setTimeout(function() { console.log('setTimeout1'); }, 100); Promise.resolve().then(function() { console.log('promise1'); }).then(function() { console.log('promise2'); }); setTimeout(function() { console.log('setTimeout2'); }, 0); console.log('script end');
輸出:
script start script end promise1 promise2 setTimeout2 setTimeout1
Promise都在setTimeout前面執行了,這是為什么?
事件循環並不僅僅只有事件隊列,而是具有至少兩個隊列,除了事件,還要保持瀏覽器執行的其他操作,而這些操作也被任務,並且分為兩類:macrotask(宏任務)和microtask(微任務),在ECMAScript中,microtask稱為jobs,macrotask稱為task。
那它們到底有什么區別呢?
① macrotask(宏任務),可以理解是每次執行棧執行的代碼就是一個宏任務。主要包括創建主文檔對象、解析 HTML、執行主線(或全局)JavaScript
代碼,更改當前 URL 以及各種事件,如頁面加載、輸入、網絡事件和定時器事件。從瀏覽器的角度來看,宏任務代表一個個離散的、獨立工作單元。運行完任務后,瀏覽器可以繼續其他調度,如重新渲染頁面的 UI 或執行垃圾回收。
② microtask(微任務)是更小的任務。微任務更新應用程序的狀態,但必須在瀏覽器任務繼續執行其他任務之前執行,瀏覽器任務包括重新渲染頁面的UI。微任務的案例包括 promise
回調函數、DOM 發生變化等。微任務需要盡可能快地、通過異步方式執行,同時不能產生全新的微任務。微任務使得我們能夠在重新渲染UI之前執行指定的行為,避免不必要的UI重繪,UI重繪會使應用程序的狀態不連續。
事件循環的實現至少應該包含有一個用於宏事件的隊列和一個用於微事件的隊列。這使得事件循環能夠根據任務類型進行優先處理。
總結起來也就是以下幾步:
① 執行一個宏任務(棧中沒有就從事件隊列中獲取)
② 執行過程中如果遇到微任務,就將它添加到微任務的任務隊列中
③ 宏任務執行完畢后,立即執行當前微任務隊列中的所有微任務(依次執行)
④ 當前宏任務執行完畢,開始檢查渲染,然后GUI線程接管渲染
⑤ 渲染完畢后,JS線程繼續接管,開始下一個宏任務(從事件隊列中獲取)
也正是由於上述的規則,在上面的那段代碼中,首先會執行主代碼塊這個宏事件,然后再將微事件Promise執行,最后再去執行第二個宏事件setTimeout
三、事件循環與渲染
前面我們提到,微事件會在宏事件與頁面渲染之間執行,那么是事件循環與頁面渲染到底存在着怎樣的聯系呢?
首先我們要知道的是瀏覽器通常會嘗試每秒渲染60次頁面,從而達到每秒60幀的數組。而60fps通常是檢驗體驗是否平滑流暢的標准,比方在動畫里——這意味着瀏覽器會嘗試在 16ms 內渲染一幀。需要注意上圖的的“更新渲染”是如何發生為何事件循環內的,因為在頁面渲染時,任何任務都無法再進行修改。這些設計和原則都意味着,如果想要實現平滑流暢的應用,我們是沒有太多時間浪費在處理單個事件循環任務的。因此,在理想情況下,單個任務和該任務附屬的所有微任務,都應在 16ms 內完成。
因此出於這種考慮,可能會出現以下的三種情況:
① 在另一個 16ms 結束前,事件循環執行到“是否需要進行渲染”的決策環節。因為更新 UI 是一個復雜的操作,所以如果沒有顯式地指定需要頁面渲染,瀏覽
器可能不會選擇在當前的循環中執行 UI 渲染操作。
② 在最后一次渲染完成后大約 16ms,時間循環執行到“是否需要進行渲染”的決策環節。在這種情況下,瀏覽器會進行 UI 更新,以便用戶能夠感受到順暢的
應用體驗。
③ 執行下一個任務(和相關的所有微任務)耗時超過 16ms。在這種情況下,瀏覽器將無法以目標幀率重新渲染頁面,且 UI 無法被更新。如果任務代碼的執行不耗費過多的時間(不超過幾百毫秒),這時的延遲甚至可能察覺不到,尤其當頁面中沒有太多的操作時。反之,如果耗時過多,或者頁面上運行有動畫時,用戶可能會察覺到網絡卡頓而不響應。在極端的情況下,如果任務的執行超過幾秒,用戶的瀏覽器將會提示“無響應腳本”的惱人信息。