W3Cschool
恭喜您成為首批注冊用戶
獲得88經(jīng)驗值獎勵
瀏覽器中 JavaScript 的執(zhí)行流程和 Node.js 中的流程都是基于 事件循環(huán) 的。
理解事件循環(huán)的工作方式對于代碼優(yōu)化很重要,有時對于正確的架構(gòu)也很重要。
在本章中,我們首先介紹事件循環(huán)工作方式的理論細節(jié),然后介紹該知識的實際應(yīng)用。
事件循環(huán) 的概念非常簡單。它是一個在 JavaScript 引擎等待任務(wù),執(zhí)行任務(wù)和進入休眠狀態(tài)等待更多任務(wù)這幾個狀態(tài)之間轉(zhuǎn)換的無限循環(huán)。
引擎的一般算法:
當我們?yōu)g覽一個網(wǎng)頁時就是上述這種形式。JavaScript 引擎大多數(shù)時候不執(zhí)行任何操作,它僅在腳本/處理程序/事件激活時執(zhí)行。
任務(wù)示例:
<script src="...">
? 加載完成時,任務(wù)就是執(zhí)行它。mousemove
? 事件和執(zhí)行處理程序。setTimeout
? 時間到達時,任務(wù)就是執(zhí)行其回調(diào)。設(shè)置任務(wù) —— 引擎處理它們 —— 然后等待更多任務(wù)(即休眠,幾乎不消耗 CPU 資源)。
一個任務(wù)到來時,引擎可能正處于繁忙狀態(tài),那么這個任務(wù)就會被排入隊列。
多個任務(wù)組成了一個隊列,即所謂的“宏任務(wù)隊列”(v8 術(shù)語):
例如,當引擎正在忙于執(zhí)行一段 script
時,用戶可能會移動鼠標而產(chǎn)生 mousemove
事件,setTimeout
或許也剛好到期,以及其他任務(wù),這些任務(wù)組成了一個隊列,如上圖所示。
隊列中的任務(wù)基于“先進先出”的原則執(zhí)行。當瀏覽器引擎執(zhí)行完 script
后,它會處理 mousemove
事件,然后處理 setTimeout
處理程序,依此類推。
到目前為止,很簡單,對吧?
兩個細節(jié):
以上是理論知識。現(xiàn)在,讓我們來看看如何應(yīng)用這些知識。
假設(shè)我們有一個 CPU 過載任務(wù)。
例如,語法高亮(用來給本頁面中的示例代碼著色)是相當耗費 CPU 資源的任務(wù)。為了高亮顯示代碼,它執(zhí)行分析,創(chuàng)建很多著了色的元素,然后將它們添加到文檔中 —— 對于文本量大的文檔來說,需要耗費很長時間。
當引擎忙于語法高亮時,它就無法處理其他 DOM 相關(guān)的工作,例如處理用戶事件等。它甚至可能會導致瀏覽器“中斷(hiccup)”甚至“掛起(hang)”一段時間,這是不可接受的。
我們可以通過將大任務(wù)拆分成多個小任務(wù)來避免這個問題。高亮顯示前 100 行,然后使用 setTimeout
(延時參數(shù)為 0)來安排(schedule)后 100 行的高亮顯示,依此類推。
為了演示這種方法,簡單起見,讓我們寫一個從 1
數(shù)到 1000000000
的函數(shù),而不寫文本高亮。
如果你運行下面這段代碼,你會看到引擎會“掛起”一段時間。對于服務(wù)端 JS 來說這顯而易見,并且如果你在瀏覽器中運行它,嘗試點擊頁面上其他按鈕時,你會發(fā)現(xiàn)在計數(shù)結(jié)束之前不會處理其他事件。
let i = 0;
let start = Date.now();
function count() {
// 做一個繁重的任務(wù)
for (let j = 0; j < 1e9; j++) {
i++;
}
alert("Done in " + (Date.now() - start) + 'ms');
}
count();
瀏覽器甚至可能會顯示一個“腳本執(zhí)行時間過長”的警告。
讓我們使用嵌套的 setTimeout
調(diào)用來拆分這個任務(wù):
let i = 0;
let start = Date.now();
function count() {
// 做繁重的任務(wù)的一部分 (*)
do {
i++;
} while (i % 1e6 != 0);
if (i == 1e9) {
alert("Done in " + (Date.now() - start) + 'ms');
} else {
setTimeout(count); // 安排(schedule)新的調(diào)用 (**)
}
}
count();
現(xiàn)在,瀏覽器界面在“計數(shù)”過程中可以正常使用。
單次執(zhí)行 count
會完成工作 (*)
的一部分,然后根據(jù)需要重新安排(schedule)自身的執(zhí)行 (**)
:
i=1...1000000
?。i=1000001..2000000
?。現(xiàn)在,如果在引擎忙于執(zhí)行第一部分時出現(xiàn)了一個新的副任務(wù)(例如 onclick
事件),則該任務(wù)會被排入隊列,然后在第一部分執(zhí)行結(jié)束時,并在下一部分開始執(zhí)行前,會執(zhí)行該副任務(wù)。周期性地在兩次 count
執(zhí)行期間返回事件循環(huán),這為 JavaScript 引擎提供了足夠的“空氣”來執(zhí)行其他操作,以響應(yīng)其他的用戶行為。
值得注意的是這兩種變體 —— 是否使用了 setTimeout
對任務(wù)進行拆分 —— 在執(zhí)行速度上是相當?shù)?。在?zhí)行計數(shù)的總耗時上沒有多少差異。
為了使兩者耗時更接近,讓我們來做一個改進。
我們將要把調(diào)度(scheduling)移動到 count()
的開頭:
let i = 0;
let start = Date.now();
function count() {
// 將調(diào)度(scheduling)移動到開頭
if (i < 1e9 - 1e6) {
setTimeout(count); // 安排(schedule)新的調(diào)用
}
do {
i++;
} while (i % 1e6 != 0);
if (i == 1e9) {
alert("Done in " + (Date.now() - start) + 'ms');
}
}
count();
現(xiàn)在,當我們開始調(diào)用 count()
時,會看到我們需要對 count()
進行更多調(diào)用,我們就會在工作前立即安排(schedule)它。
如果你運行它,你很容易注意到它花費的時間明顯減少了。
為什么?
這很簡單:你應(yīng)該還記得,多個嵌套的 setTimeout
調(diào)用在瀏覽器中的最小延遲為 4ms。即使我們設(shè)置了 0
,但還是 4ms
(或者更久一些)。所以我們安排(schedule)得越早,運行速度也就越快。
最后,我們將一個繁重的任務(wù)拆分成了幾部分,現(xiàn)在它不會阻塞用戶界面了。而且其總耗時并不會長很多。
對瀏覽器腳本中的過載型任務(wù)進行拆分的另一個好處是,我們可以顯示進度指示。
正如前面所提到的,僅在當前運行的任務(wù)完成后,才會對 DOM 中的更改進行繪制,無論這個任務(wù)運行花費了多長時間。
從一方面講,這非常好,因為我們的函數(shù)可能會創(chuàng)建很多元素,將它們一個接一個地插入到文檔中,并更改其樣式 —— 訪問者不會看到任何未完成的“中間態(tài)”內(nèi)容。很重要,對吧?
這是一個示例,對 i
的更改在該函數(shù)完成前不會顯示出來,所以我們將只會看到最后的值:
<div id="progress"></div>
<script>
function count() {
for (let i = 0; i < 1e6; i++) {
i++;
progress.innerHTML = i;
}
}
count();
</script>
……但是我們也可能想在任務(wù)執(zhí)行期間展示一些東西,例如進度條。
如果我們使用 setTimeout
將繁重的任務(wù)拆分成幾部分,那么變化就會被在它們之間繪制出來。
這看起來更好看:
<div id="progress"></div>
<script>
let i = 0;
function count() {
// 做繁重的任務(wù)的一部分 (*)
do {
i++;
progress.innerHTML = i;
} while (i % 1e3 != 0);
if (i < 1e7) {
setTimeout(count);
}
}
count();
</script>
現(xiàn)在 div
顯示了 i
的值的增長,這就是進度條的一種。
在事件處理程序中,我們可能會決定推遲某些行為,直到事件冒泡并在所有級別上得到處理后。我們可以通過將該代碼包裝到零延遲的 setTimeout
中來做到這一點。
在 創(chuàng)建自定義事件 一章中,我們看到過這樣一個例子:自定義事件 menu-open
被在 setTimeout
中分派(dispatched),所以它在 click
事件被處理完成之后發(fā)生。
menu.onclick = function() {
// ...
// 創(chuàng)建一個具有被點擊的菜單項的數(shù)據(jù)的自定義事件
let customEvent = new CustomEvent("menu-open", {
bubbles: true
});
// 異步分派(dispatch)自定義事件
setTimeout(() => menu.dispatchEvent(customEvent));
};
除了本章中所講的 宏任務(wù)(macrotask) 外,還有在 微任務(wù)(Microtask) 一章中提到的 微任務(wù)(microtask)。
微任務(wù)僅來自于我們的代碼。它們通常是由 promise 創(chuàng)建的:對 .then/catch/finally
處理程序的執(zhí)行會成為微任務(wù)。微任務(wù)也被用于 await
的“幕后”,因為它是 promise 處理的另一種形式。
還有一個特殊的函數(shù) queueMicrotask(func)
,它對 func
進行排隊,以在微任務(wù)隊列中執(zhí)行。
每個宏任務(wù)之后,引擎會立即執(zhí)行微任務(wù)隊列中的所有任務(wù),然后再執(zhí)行其他的宏任務(wù),或渲染,或進行其他任何操作。
例如,看看下面這個示例:
setTimeout(() => alert("timeout"));
Promise.resolve()
.then(() => alert("promise"));
alert("code");
這里的執(zhí)行順序是怎樣的?
code
? 首先顯示,因為它是常規(guī)的同步調(diào)用。promise
? 第二個出現(xiàn),因為 ?then
? 會通過微任務(wù)隊列,并在當前代碼之后執(zhí)行。timeout
? 最后顯示,因為它是一個宏任務(wù)。更詳細的事件循環(huán)圖示如下(順序是從上到下,即:首先是腳本,然后是微任務(wù),渲染等):
微任務(wù)會在執(zhí)行任何其他事件處理,或渲染,或執(zhí)行任何其他宏任務(wù)之前完成。
這很重要,因為它確保了微任務(wù)之間的應(yīng)用程序環(huán)境基本相同(沒有鼠標坐標更改,沒有新的網(wǎng)絡(luò)數(shù)據(jù)等)。
如果我們想要異步執(zhí)行(在當前代碼之后)一個函數(shù),但是要在更改被渲染或新事件被處理之前執(zhí)行,那么我們可以使用 queueMicrotask
來對其進行安排(schedule)。
這是一個與前面那個例子類似的,帶有“計數(shù)進度條”的示例,但是它使用了 queueMicrotask
而不是 setTimeout
。你可以看到它在最后才渲染。就像寫的是同步代碼一樣:
<div id="progress"></div>
<script>
let i = 0;
function count() {
// 做繁重的任務(wù)的一部分 (*)
do {
i++;
progress.innerHTML = i;
} while (i % 1e3 != 0);
if (i < 1e6) {
queueMicrotask(count);
}
}
count();
</script>
更詳細的事件循環(huán)算法(盡管與 規(guī)范 相比仍然是簡化過的):
安排(schedule)一個新的 宏任務(wù):
setTimeout(f)
?。它可被用于將繁重的計算任務(wù)拆分成多個部分,以使瀏覽器能夠?qū)τ脩羰录鞒龇磻?yīng),并在任務(wù)的各部分之間顯示任務(wù)進度。
此外,也被用于在事件處理程序中,將一個行為(action)安排(schedule)在事件被完全處理(冒泡完成)后。
安排一個新的 微任務(wù):
queueMicrotask(f)
?。在微任務(wù)之間沒有 UI 或網(wǎng)絡(luò)事件的處理:它們一個立即接一個地執(zhí)行。
所以,我們可以使用 ?queueMicrotask
? 來在保持環(huán)境狀態(tài)一致的情況下,異步地執(zhí)行一個函數(shù)。
Web Workers
對于不應(yīng)該阻塞事件循環(huán)的耗時長的繁重計算任務(wù),我們可以使用 Web Workers。
這是在另一個并行線程中運行代碼的方式。
Web Workers 可以與主線程交換消息,但是它們具有自己的變量和事件循環(huán)。
Web Workers 沒有訪問 DOM 的權(quán)限,因此,它們對于同時使用多個 CPU 內(nèi)核的計算非常有用。
console.log(1);
setTimeout(() => console.log(2));
Promise.resolve().then(() => console.log(3));
Promise.resolve().then(() => setTimeout(() => console.log(4)));
Promise.resolve().then(() => console.log(5));
setTimeout(() => console.log(6));
console.log(7);
輸出結(jié)果為:1 7 3 5 2 6 4。
這道題其實很簡單,我們只需要知道微任務(wù)和宏任務(wù)隊列是如何工作的。
讓我們一起一步一步地看看發(fā)生了什么。
console.log(1);
// 第一行立即執(zhí)行,它輸出 `1`。
// 到目前為止,宏任務(wù)隊列和微任務(wù)隊列都是空的。
setTimeout(() => console.log(2));
// `setTimeout` 將回調(diào)添加到宏任務(wù)隊列。
// - 宏任務(wù)隊列中的內(nèi)容:
// `console.log(2)`
Promise.resolve().then(() => console.log(3));
// 將回調(diào)添加到微任務(wù)隊列。
// - 微任務(wù)隊列中的內(nèi)容:
// `console.log(3)`
Promise.resolve().then(() => setTimeout(() => console.log(4)));
// 帶有 `setTimeout(...4)` 的回調(diào)被附加到微任務(wù)隊列。
// - 微任務(wù)隊列中的內(nèi)容:
// `console.log(3); setTimeout(...4)`
Promise.resolve().then(() => console.log(5));
// 回調(diào)被添加到微任務(wù)隊列
// - 微任務(wù)隊列中的內(nèi)容:
// `console.log(3); setTimeout(...4); console.log(5)`
setTimeout(() => console.log(6));
// `setTimeout` 將回調(diào)添加到宏任務(wù)隊列
// - 宏任務(wù)隊列中的內(nèi)容:
// `console.log(2); console.log(6)`
console.log(7);
// 立即輸出 7
總結(jié)一下:
1
? 和 ?7
?,因為簡單的 ?console.log
? 調(diào)用沒有使用任何隊列。console.log(3); setTimeout(...4); console.log(5)
?。3
? 和 ?5
?,?setTimeout(() => console.log(4))
? 將 ?console.log(4)
? 調(diào)用添加到了宏任務(wù)隊列的尾部。console.log(2); console.log(6); console.log(4)
?。2
?、?6
? 和 ?4
?。
最終,我們的到的輸出結(jié)果為:1 7 3 5 2 6 4
。
Copyright©2021 w3cschool編程獅|閩ICP備15016281號-3|閩公網(wǎng)安備35020302033924號
違法和不良信息舉報電話:173-0602-2364|舉報郵箱:jubao@eeedong.com
掃描二維碼
下載編程獅App
編程獅公眾號
聯(lián)系方式:
更多建議: