文章翻譯自:
https://javascript.info/event-loop在這片文章,我們要帶著兩個問題去學習
事件循環瀏覽器 js 以及 Nodejs 都是基於事件循環,了解事件循環對於代碼優化非常重要。在本章中,我們首先介紹有關事物如何工作的理論細節,然後介紹該知識的實際應用。
就是有一個無限循環機制:JavaScript 引擎等待任務,執行任務,然後休眠,等待更多任務。
引擎的一般算法這是瀏覽頁面時看到的形式化信息。JavaScript 引擎大部分時間不執行任何操作,僅在腳本/處理程序/事件激活時運行。
任務示例<script src="...">加載外部腳本時,任務是執行它用戶移動滑鼠時,任務是調度 mousemove 事件並執行處理程序當計劃好的時間到了 setTimeout,任務是運行其回調。設置任務-引擎處理它們-然後等待更多任務(在睡眠時消耗接近零的CPU)。
引擎繁忙時可能會發生任務,然後將其排入隊列。
任務形成一個隊列,即所謂的「宏任務隊列」(v8術語):例如,當引擎正忙於執行 script,用戶可能會移動滑鼠 mousemove,這 setTimeout 可能是由於任務到期而導致的,等等,這些任務形成了一個隊列,如上圖所示。
隊列中的任務按「先到先得」的原則處理。引擎瀏覽器用完成後 script,它將處理 mousemove 事件,然後 setTimeout 處理程序,依此類推。
到目前為止,很簡單,對吧?
另外兩個細節:引擎執行任務時永遠不會進行渲染。任務是否花費很長時間都沒關係。僅在任務完成後才繪製對 DOM 的更改。如果一項任務花費的時間太長,瀏覽器將無法執行其他任務,例如處理用戶事件。因此,過了一會兒,它會發出「頁面無響應」之類的警報,建議終止整個頁面的任務。當存在大量複雜的計算或導致無限循環的編程錯誤時,就會發生這種情況。用例1:分割 CPU 任務假設我們有一個需要 CPU 的任務。
例如,語法高亮(用於著色此頁面上的代碼示例)相當佔用 CPU 資源。為了突出顯示代碼,它執行分析,創建許多彩色元素,然後將它們添加到文檔中-花費大量時間編寫大量文本。
當引擎忙於語法高亮顯示時,它無法執行其他與 DOM 相關的工作,處理用戶事件等。它甚至可能導致瀏覽器「打ic」甚至「掛起」一小段時間,這是不可接受的。
通過將大任務分成多個部分,我們可以避免問題。突出顯示前100行,然後為後100行計劃 setTimeout(零延遲),依此類推。
為了證明這種方法,為簡單起見,而不是文本的高亮顯示,讓我們一個函數,計算從1到1000000000。
如果您運行下面的代碼,引擎將「掛起」一段時間。對於明顯可見的伺服器端JS,如果您正在瀏覽器中運行它,則嘗試單擊頁面上的其他按鈕–您會發現在計數結束之前不會處理其他事件。
let i = 0;
let start = Date.now();
function count() {
// do a heavy job
for (let j = 0; j < 1e9; j++) {
i++;
}
alert("Done in " + (Date.now() - start) + 'ms');
}
count();瀏覽器甚至可能顯示「腳本花費太長時間」的警告。
讓我們使用嵌套 setTimeout 調用拆分作業:
let i = 0;
let start = Date.now();
function count() {
// do a piece of the heavy job (*)
do {
i++;
} while (i % 1e6 != 0);
if (i == 1e9) {
alert("Done in " + (Date.now() - start) + 'ms');
} else {
setTimeout(count); // schedule the new call (**)
}
}
count();現在,瀏覽器界面在「計數」過程中可以正常使用。
一次運行 count 完成一部分工作,然後根據需要重新計劃自身:
第二次運行計數:i=1000001..2000000。現在,如果 onclick 在引擎正在忙於執行第1部分時出現新的輔助任務(例如事件),則將其排隊,然後在第1部分完成時在下一部分之前執行。count 執行之間定期返回事件循環為 JavaScript 引擎提供了足夠的「空氣」來執行其他操作,以對其他用戶操作做出反應。
值得注意的是,兩種變體(無論是否分配工作)setTimeout在速度上都是可比的。總體計數時間沒有太大差異。
為了使它們更接近,讓我們進行改進。
我們將排程移至的開頭 count():
let i = 0;
let start = Date.now();
function count() {
// move the scheduling to the beginning
if (i < 1e9 - 1e6) {
setTimeout(count); // schedule the new call
}
do {
i++;
} while (i % 1e6 != 0);
if (i == 1e9) {
alert("Done in " + (Date.now() - start) + 'ms');
}
}
count();現在,當我們開始 count() 發現需要做 count() 更多的事情時,我們會立即安排工作時間,然後再進行這項工作。
如果您運行它,很容易注意到它花費的時間大大減少。
為什麼?這很簡單:您記得,許多嵌套 setTimeout 調用在瀏覽器中的最小延遲為4ms 。即使我們設置了0,它4ms(或者更多)。因此,我們計劃得越早–運行速度越快。
最後,我們將需要大量 CPU 的任務分成了幾個部分–現在它不會阻塞用戶界面。而且它的整體執行時間不會更長。
用例2:進度指示為瀏覽器腳本分配繁重任務的另一個好處是,我們可以顯示進度指示。
如前所述,僅在當前運行的任務完成後才繪製對DOM的更改,而不管它花費多長時間。
一方面,這很棒,因為我們的函數可能創建許多元素,將它們一個接一個地添加到文檔中並更改其樣式-訪問者將看不到任何「中間」未完成的狀態。重要的是吧?
這是演示,在i功能完成之前不會顯示對的更改,因此我們將僅看到最後一個值:
<div id="progress"></div>
<script>
function count() {
for (let i = 0; i < 1e6; i++) {
i++;
progress.innerHTML = i;
}
}
count();
</script>…但是我們也可能希望在任務執行過程中顯示一些東西,例如進度條。
如果我們使用來將繁重的任務分成幾部分 setTimeout,那麼更改將被繪製在它們之間。
這看起來更漂亮:
<div id="progress"></div>
<script>
let i = 0;
function count() {
// do a piece of the heavy job (*)
do {
i++;
progress.innerHTML = i;
} while (i % 1e3 != 0);
if (i < 1e7) {
setTimeout(count);
}
}
count();
</script>現在,<div>顯示的是的增加值 i,這是一種進度條。
用例3:在事件發生後採取措施在事件處理程序中,我們可能會決定推遲一些操作,直到事件冒泡並在所有級別上得到處理。我們可以通過將代碼包裝為零延遲來實現 setTimeout。
在分派自定義事件一章中,我們看到了一個示例:自定義事件 menu-open 是在中分派的 setTimeout ,因此它在完全處理「 click」事件之後發生。
menu.onclick = function() {
// ...
// create a custom event with the clicked menu item data
let customEvent = new CustomEvent("menu-open", {
bubbles: true
});
// dispatch the custom event asynchronously
setTimeout(() => menu.dispatchEvent(customEvent));
};
宏任務和微任務隨著宏任務,在本章中所描述的,有 microtasks,在章節中提到 Microtasks。
微任務僅來自我們的代碼。它們通常是由.then/catch/finallyPromise創建的:處理程序的執行成為微任務。微任務也被「秘密使用」 await,因為它是承諾處理的另一種形式。
還有一個特殊功能queueMicrotask(func),func 可以在微任務隊列中排隊等待執行。
每一個後立即宏任務時,引擎執行所有任務 microtask 隊列運行任何其他宏任務或渲染或其他任何東西之前,。
例如,看一下:
setTimeout(() => alert("timeout"));
Promise.resolve()
.then(() => alert("promise"));
alert("code");這將是什麼順序?
promise顯示第二個,因為它.then通過微任務隊列,並在當前代碼之後運行。更豐富的事件循環圖片如下所示(順序是從上到下,即:首先是腳本,然後是微任務,渲染,等等):
在執行任何其他事件處理或呈現或執行任何其他宏任務之前,所有微任務都已完成。
這很重要,因為它可以確保微任務之間的應用程式環境基本相同(沒有滑鼠坐標更改,沒有新的網絡數據等)。
如果我們想異步執行一個函數(在當前代碼之後),但是在呈現更改或處理新事件之前,可以使用進行調度queueMicrotask。
這是一個帶有「計數進度條」的示例,與之前顯示的示例相似,但queueMicrotask用於代替setTimeout。您可以看到它在最後渲染。就像同步代碼一樣:
<div id="progress"></div>
<script>
let i = 0;
function count() {
// do a piece of the heavy job (*)
do {
i++;
progress.innerHTML = i;
} while (i % 1e3 != 0);
if (i < 1e6) {
queueMicrotask(count);
}
}
count();
</script>
概要更詳細的事件循環算法(儘管與規範相比仍簡化了):
1從宏任務隊列中出隊並運行最早的任務(例如「腳本」)。2執行所有微任務:- 當微任務隊列不為空時:- 出隊並運行最舊的微任務。要安排新的宏任務:
這可用於將繁重的計算任務分解為多個部分,以使瀏覽器能夠對用戶事件做出反應並顯示它們之間的進度。
另外,在事件處理程序中用於安排事件完全處理(冒泡完成)後的操作。
安排新的微任務
微任務之間沒有 UI 或網絡事件處理:它們立即接連運行。
因此,您可能想queueMicrotask 異步執行功能,但要在環境狀態下執行。