提問
▼ 請問點擊哪個按鈕會導致頁面卡死飘诗?
<article>
<h1>蒹葭</h1>
<p>蒹葭蒼蒼圆丹,白露為霜。所謂伊人撒妈,在水一方恢暖。溯洄從之,道阻且長狰右。溯游從之杰捂,宛在水中央。</p>
<p>蒹葭萋萋棋蚌,白露未晞嫁佳。所謂伊人,在水之湄谷暮。溯洄從之蒿往,道阻且躋。溯游從之湿弦,宛在水中坻瓤漏。</p>
<p>蒹葭采采,白露未已颊埃。所謂伊人蔬充,在水之涘。溯洄從之班利,道阻且右饥漫。溯游從之,宛在水中沚罗标。</p>
<p></p>
</article>
<button onclick="whileLoop()">點我</button>
<button onclick="timerLoop()">點我</button>
<button onclick="promiseLoop()">點我</button>
<script>
function whileLoop() {
while (true) {}
}
function timerLoop() {
setTimeout(timerLoop, 0)
}
function promiseLoop() {
Promise.resolve().then(promiseLoop)
}
</script>
▼ 請問點擊按鈕紅色 div 會閃嗎庸队?
<div id="box" style="width: 100px; height: 100px; background: red"></div>
<button onclick="clickme">點我</button>
<script>
const box = document.getElementById('box')
function clickme() {
box.style.display = 'none'
box.style.display = 'block'
box.style.display = 'none'
box.style.display = 'block'
box.style.display = 'none'
box.style.display = 'block'
})
</script>
題目比較簡單,相信大家都有答案了闯割。
我們繼續(xù)往下彻消。
開始之前
對于 Event Loop,相信大家都有這樣一張圖:
接下來宙拉,將會更深入地了解 Event Loop证膨,真的如上圖所示嗎?
沒錯鼓黔,我也是從以下鏈接受益央勒,并結(jié)合自己的理解,將其寫下來而已澳化。
- Philip Roberts: What the heck is the event loop anyway?
- Erin Zimmer: Further Adventures of the Event Loop
- Jake Archibald: In The Loop
為什么 JavaScript 設計成單線程崔步?
最初 JavaScript 是為瀏覽器而設計的,旨在增強可交互性缎谷。
單線程井濒,意味著同一時間只能做一件事情灶似。
設想一下,有兩個線程同時作用于某個元素瑞你,一個是修改樣式酪惭,另一個是刪除元素,如何響應呢者甲?引入鎖機制春感?
當時網(wǎng)頁不如現(xiàn)在復雜,選擇單線程是明智虏缸、合理鲫懒、夠用的,操作變得有序可控刽辙,且大大降低復雜度窥岩。
隨著時代的發(fā)展,計算越來越復雜宰缤,單線程有點捉襟見肘颂翼,后來 HTML5 提供了 Web Worker 等 API 可主動創(chuàng)建新的線程運行一些復雜的運算。
什么是 Event Loop?
規(guī)范是這樣定義的:
To coordinate events, user interaction, scripts, rendering, networking, and so forth.
協(xié)調(diào)事件慨灭、用戶交互朦乏、腳本、渲染缘挑、網(wǎng)絡等集歇。
個人理解:它是讓各種任務有序可控的一種機制桶略。
用偽代碼表示:
while (true) {
task = taskQueue.pop()
execute(task)
}
當然语淘,實際沒有這么簡單,只是從簡單說起际歼,請繼續(xù)往下惶翻。
它是無限循環(huán)的,7 × 24h 隨時待命鹅心,直至瀏覽器 Tab 被關閉吕粗。
只要有任務,它就會不停地從隊列中取出任務旭愧,執(zhí)行任務颅筋。
在瀏覽器中,Event Loop 有 Window Event Loop输枯、Worker Event Loop议泵、Worklet Event Loop 三種,第一種是本文主要討論的對象桃熄。當然 Node.js 也有 Event Loop 機制先口,但不太一樣。
什么是 Task?
規(guī)范是這樣定義的:
Formally, a task is a struct which has: steps, a source, a document, a script evaluation environment settings object set.
形式上碉京,任務是一種 struct 結(jié)構(gòu)體厢汹,包含 Steps、Source谐宙、Document烫葬、Script evaluation environment settings object set。
簡單來說卧惜,任務就是一個包含 steps 等屬性的對象厘灼,里面記錄了任務的來源、所屬 Document 對象咽瓷、上下文等设凹,以供后續(xù)調(diào)度。
常見的任務有:
- 與用戶發(fā)生交互而產(chǎn)生的所有事件回調(diào)(比如單擊茅姜、文本選擇闪朱、頁面滾動、鍵盤輸入等)
- setTimeout钻洒、setInterval
- 執(zhí)行 script 塊
- I/O 操作
什么是 Task Queue奋姿?
常規(guī)意義的隊列
隊列(Queue)是一種基本的數(shù)據(jù)結(jié)構(gòu),遵循先進先出(FIFO, First In First Out)的原則素标。在隊列中称诗,最先插入的元素最先被移除,類似于排隊等候的場景头遭。
- 入隊(Enqueue):將一個元素添加到隊列的尾部寓免。
- 出隊(Dequeue):從隊列的頭部移除一個元素,并返回該元素计维。
Event Loop 中的任務隊列
規(guī)范中提到:
An event loop has one or more task queues.
事件循環(huán)有一個或多個任務隊列袜香。
Task queues are sets, not queues, because the event loop processing model grabs the first runnable task from the chosen queue, instead of dequeuing the first task.'
任務隊列是集合,而不是隊列鲫惶,因為事件循環(huán)處理模型從所選隊列中獲取第一個可運行的任務蜈首,而不是使第一個任務出隊。
The microtask queue is not a task queue.
微任務隊列不是任務隊列欠母。
前面提到欢策,task 是有 source 的,比如來自鼠標點擊等赏淌。排隊時踩寇,同 source 的 task 會被放入與該 source 相關的 task queue 里。假設鼠標事件的任務要優(yōu)于其他任務猜敢,Event Loop 就可以在對應 source 的 task queue 中取出任務優(yōu)先執(zhí)行姑荷。規(guī)范里 Event Loop 執(zhí)行步驟并沒有明確定義“出隊”的規(guī)則盒延,它取決于瀏覽器的實現(xiàn)。
Let taskQueue be one such task queue, chosen in an implementation-defined manner.
現(xiàn)在 Event Loop 用偽代碼表示是這樣的:
while (true) {
queue = getNextQueue()
task = queue.getFirstRunnableTask()
execute(task)
}
?? 在此之前鼠冕,我的認知是:一個 Event Loop 里有且只有一個任務隊列添寺,且它是一個常規(guī)意義的隊列。雖說如此懈费,如果只想了解 Event Loop 主要執(zhí)行順序计露,不深入瀏覽器究竟維護了多少個任務隊列、瀏覽器如何決定下一任務憎乙,按原來的理解也問題不大扒俯。
什么時候重繪頁面毕骡?
總不能只執(zhí)行任務,不更新 DOM 吧。
本質(zhì)上废膘,網(wǎng)頁就是給人看的关噪,與人交互的衔憨,所以用戶體驗非常重要严卖。假設任務隊列有源源不斷的任務產(chǎn)生,如果 Event Loop 只會一直循環(huán)執(zhí)行隊列里的任務梢什,而不去更新頁面奠蹬,用戶體驗是非常糟糕的。
請問瀏覽器什么時候會更新頁面嗡午?
瀏覽器是非常聰明的囤躁,沒必要的工作它不會做。以 60Hz 屏幕為例荔睹,每秒刷新 60 次狸演,約 16.7ms 刷新一次。只要滿足該刷新頻率的应媚,顯示就算是流暢的严沥,因為再快的刷新頻率對肉眼來說也不會有明顯的感知猜极。也就是說每 16.7ms 可獲得一次渲染機會(rendering opportunity)中姜,這樣瀏覽器就知道要更新 DOM 了。
假設一個任務耗時 3 ~ 5ms跟伏,遠沒到 16.7ms丢胚,對于瀏覽器來說,此時更新 DOM 是沒有必要的受扳,因此也不會獲得一個渲染機會携龟。相反地,如果一個任務執(zhí)行超過 16.7ms勘高,呈現(xiàn)出來的效果有可能是卡頓的峡蟋。
注意坟桅,規(guī)范中不強制要求使用任何特定模型來選擇渲染機會。但例如蕊蝗,如果瀏覽器嘗試實現(xiàn) 60Hz 刷新率仅乓,則渲染機會最多每 60 秒出現(xiàn)一次(約 16.7ms)。如果瀏覽器發(fā)現(xiàn) navigable 無法維持此速率蓬戚,則該 navigable 可能會下降到更可持續(xù)的每秒 30 個渲染機會夸楣,而不是偶爾丟幀。類似地子漩,如果 navigable 不可見豫喧,瀏覽器可能會決定將該頁面降低到每秒 4 個渲染機會,甚至更少幢泼。
React 16 可中斷的調(diào)度機制紧显,就是為了可以執(zhí)行優(yōu)先級更高的任務(比如更新 DOM),以解決某些場景下頁面卡頓的問題缕棵。
因此鸟妙,一個任務執(zhí)行完,如果有渲染機會先更新 DOM挥吵,接著才執(zhí)行下一個任務重父。
現(xiàn)在 Event Loop 用偽代碼表示是這樣的:
while (true) {
queue = getNextQueue()
task = queue.getFirstRunnableTask()
execute(task)
if (hasRendringOpportunity()) repaint()
}
什么是 Microtask?
還沒完忽匈,還沒完...
規(guī)范中提到:
Each event loop has a microtask queue, which is a queue of microtasks, initially empty.
A microtask is a colloquial way of referring to a task that was created via the queue a microtask algorithm.
The microtask queue is not a task queue.
好房午,我們重新捋一下:
- 一個 Event Loop 有一個或多個 task queue。
- 一個 Event Loop 有且僅有一個 microtask queue丹允。
- task 是一個由特定屬性的對象(規(guī)范中稱為 struct)郭厌。
- microtask 只是一種通俗的說法,它是通過特定算法創(chuàng)建的 task雕蔽。
- task queue 是一組 task 的集合折柠,并不是隊列。
- microtask 是常規(guī)意義的隊列批狐,遵循先進先出扇售。
- microtask queue 不是 task queue,前者是隊列嚣艇,后者是集合承冰。
為便于區(qū)分理解,本文暫且將以下規(guī)范術語口語化(但注意食零,這種說法不一定準確)困乒。
- task:(宏)任務
- task queue:(宏)任務隊列
- microtask:微任務
- microtask queue:微任務隊列
有哪些微任務?
在 JavaScript 里會產(chǎn)生微任務的大概有:
- queueMicrotask(Window 或 Web Worker)
- Promise 回調(diào)
- MutationObserver 回調(diào)
- Object.observe(Deprecated)
什么時候執(zhí)行微任務贰谣?
從規(guī)范(Processing model)可知娜搂,只要 Event Loop 存在迁霎,就必須不斷執(zhí)行以下步驟:
- 從(宏)任務隊列取出一個 task
- 執(zhí)行該 task
- 執(zhí)行微任務檢查點(microtask checkpoint)
- 如果檢查點標志為真(初始值為 false),則返回(跳出微任務執(zhí)行)百宇。
- 將檢查點標志設為 true
- 如果當前 Event Loop 里的微任務隊列不為空欧引,將一直循環(huán)直至微隊列為空:
- 在微任務隊列里取出的第一個微任務
- 執(zhí)行微任務
- 將檢查點標志設為 false
- 重復上述步驟
以上為簡化后的步驟。
至此恳谎,文章開頭的提問之一就有答案芝此。由于它在執(zhí)行微任務的過程中不停地產(chǎn)生新的微任務,因此將會在 3.iii 陷入死循環(huán)因痛,自然頁面就“卡死”了婚苹。
跟 task 的一些區(qū)別
請注意,無論是(宏)任務鸵膏,還是微任務膊升,執(zhí)行過程中都可能產(chǎn)生“新”的(宏)任務或微任務。它們的執(zhí)行順序是有區(qū)別的:
- (宏)任務執(zhí)行時產(chǎn)生的新的(宏)任務谭企,在下一輪或以后執(zhí)行廓译。
- (宏)任務執(zhí)行時產(chǎn)生的新的微任務,在當前(宏)任務執(zhí)行完之后债查、更新 DOM 或下一輪(宏)任務之前執(zhí)行非区。
- 微任務執(zhí)行時產(chǎn)生的新的(宏)任務,在下一輪或以后執(zhí)行盹廷。
- 微任務執(zhí)行時產(chǎn)生的新的微任務征绸,馬上放入微任務隊列,直到所有微任務隊列執(zhí)行完俄占,才到更新 DOM 或執(zhí)行下一輪(宏)任務管怠。
現(xiàn)在 Event Loop 用偽代碼表示是這樣的:
while (true) {
queue = getNextQueue()
task = queue.getFirstRunnableTask()
execute(task)
while (microtaskQueue.hasTask() {
microtask = microtaskQueue.pop()
excute(microtask)
}
if (hasRendringOpportunity()) repaint()
}
什么是 requestAnimationFrame?
噢缸榄,還沒完渤弛,還有一個 requestAnimationFrame,其回調(diào)函數(shù)會在頁面重繪之前調(diào)用甚带。
當瀏覽器檢測到有渲染機會她肯,會更新 DOM,具體執(zhí)行順序如下:
- 執(zhí)行 requestAnimationFrame 回調(diào)
- 合成:計算樣式欲低,將 DOM Tree 和 CSSOM Tree 合成一個 Render Tree(Attachment)
- 重排:以確定每個節(jié)點所占空間辕宏、所在位置等(Layout)
- 重繪:以設置顏色等(Paint)
比較坑的是畜晰,Edge 和 Safari 將 requestAnimationFrame 回調(diào)放到 Paint 后面執(zhí)行砾莱,這是非標準做法。也就是說凄鼻,如果回調(diào)中涉及樣式腊瑟,用戶要在下一幀才能看到變化聚假。
Safari 是否已修復,待驗證闰非。
除了有 task queue(集合)膘格、microtask queue(隊列),還有一個 animation frame callbacks财松,它是一個 ordered map(映射)瘪贱。
將 animation frame callbacks 簡單理解為“隊列”也不是不行,因為根據(jù) run the animation frame callbacks 可以看到辆毡,也是從第一個開始遍歷執(zhí)行菜秦。
同樣地,執(zhí)行 callbacks 的過程中產(chǎn)生新的 callback舶掖,它們會放到下一次 Loop 執(zhí)行球昨,這點跟微任務是不一樣的。
現(xiàn)在 Event Loop 用偽代碼表示是這樣的:
while (true) {
queue = getNextQueue()
task = queue.getFirstRunnableTask()
execute(task)
while (microtaskQueue.hasTask() {
microtask = microtaskQueue.pop()
excute(microtask)
}
if (hasRendringOpportunity()) {
callbacks = animationFrameCallbacks.spliceAll()
for (callback in callbacks) {
execute(callback)
}
repaint()
}
}
Node.js Event Loop 是怎樣的呢眨攘?
相比之下主慰,Node.js 里沒有以下這些:
- 沒有 <script> 解析
- 沒有用戶交互
- 沒有 DOM
- 沒有 requestAnimationFrame
Node.js 特有的是:
- setImmediate
- process.nextTick
Node.js 的 Event Loop 由 libuv 實現(xiàn),包含以下階段:
┌───────────────────────────┐
┌─>│ timers │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │
│ └─────────────┬─────────────┘ ┌───────────────┐
│ ┌─────────────┴─────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └─────────────┬─────────────┘ │ data, etc. │
│ ┌─────────────┴─────────────┐ └───────────────┘
│ │ check │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │
└───────────────────────────┘
- timers:執(zhí)行 setTimeout鲫售、setInterval 的回調(diào)共螺。
- pending callbacks:執(zhí)行一些系統(tǒng)操作的回調(diào),比如 TCP 錯誤等情竹。
- idle, prepare:僅在內(nèi)部使用璃谨。
- poll:幾乎所有異步回調(diào)都在這個階段執(zhí)行,除 setTimeout鲤妥、setInterval 和 setImmediate 之外佳吞。
- check:執(zhí)行 setImmediate 的回調(diào)。
- close callbacks:執(zhí)行關閉事件棉安,比如 socket 或 handle 突然關閉底扳,會發(fā)出 close 事件。
在 Node.js 中贡耽,還有一個特殊的 process.nextTick()
方法衷模。技術上,它不屬于事件循環(huán)的一部分蒲赂。當你在某個階段調(diào)用時阱冶,傳遞給它的所有回調(diào)將在當前階段執(zhí)行完之后,下一個階段執(zhí)行之前執(zhí)行滥嘴。如果遞歸調(diào)用它木蹬,是會造成死循環(huán)的。
用偽代碼表示是這樣的:
while (tasksAreWaiting()) {
queue = getNextQueue()
while (queue.hasTask()) {
task = queue.pop()
execute(task)
while (nextTickQueue.hasTask()) {
callback = nextTickQueue.pop()
excute(callback)
}
while (promiseQueue.hasTask() {
promise = promiseQueue.pop()
excute(promise)
}
}
}
Worker Event Loop 又是怎樣的呢若皱?
它更簡單:
- 沒有 <script> 解析
- 沒有用戶交互
- 沒有 DOM(Worker 不能直接操作 DOM)
- 沒有 requestAnimationFrame
- 沒有 process.nextTick
- 沒有 setImmediate
且線程之間相互獨立镊叁,每個線程都有自己的 Event Loop尘颓,互不干擾。
現(xiàn)在 Event Loop 用偽代碼表示是這樣的:
while (true) {
task = taskQueue.pop()
execute(task)
while (microtaskQueue.hasTask() {
microtask = microtaskQueue.pop()
excute(microtask)
}
}
但注意晦譬,如果在 Web Worker 的線程向主線程傳遞消息疤苹,這個消息對于 Window Event Loop 來說屬于一個 task,它仍受主線程的 Event Loop 控制敛腌,該排隊還得排隊卧土。
思考題
先回到文章開頭的題目。
點擊哪個按鈕會導致頁面卡死像樊?
<article>
<h1>蒹葭</h1>
<p>蒹葭蒼蒼夸溶,白露為霜。所謂伊人凶硅,在水一方缝裁。溯洄從之,道阻且長足绅。溯游從之捷绑,宛在水中央。</p>
<p>蒹葭萋萋氢妈,白露未晞粹污。所謂伊人,在水之湄首量。溯洄從之壮吩,道阻且躋。溯游從之加缘,宛在水中坻鸭叙。</p>
<p>蒹葭采采,白露未已拣宏。所謂伊人沈贝,在水之涘。溯洄從之勋乾,道阻且右宋下。溯游從之,宛在水中沚辑莫。</p>
<p></p>
</article>
<button onclick="whileLoop()">點我</button>
<button onclick="timerLoop()">點我</button>
<button onclick="promiseLoop()">點我</button>
<script>
function whileLoop() {
while (true) {}
}
function timerLoop() {
setTimeout(timerLoop, 0)
}
function promiseLoop() {
Promise.resolve().then(promiseLoop)
}
</script>
答案:whileLoop学歧、promiseLoop 會導致頁面卡死,timerLoop 則不會各吨。
whileLoop 分析:點擊按鈕枝笨,產(chǎn)生一個 task,進入 task queue 排隊。輪到它的時候伺帘,執(zhí)行 whileLoop() 方法昭躺,里面是一個無線循環(huán)的 while 語句忌锯,因此這個 task 會一直執(zhí)行下去伪嫁,且致使后面的 task、更新 DOM 等永遠無法執(zhí)行偶垮。頁面就卡死了张咳。
timerLoop 分析:點擊按鈕,產(chǎn)生一個 task似舵,進入 task queue 排隊脚猾。輪到它的時候,執(zhí)行 timerLoop() 方法砚哗,又產(chǎn)生一個 task 并放入 task queue龙助。執(zhí)行完之后,如果有 rendering opportunity 會先更新 DOM蛛芥,完了執(zhí)行進行下一輪提鸟。盡管 timerLoop 里不停地產(chǎn)生新的 task,但用戶仍然通過文本選擇仅淑、頁面滾動等產(chǎn)生其他 task 進入到 task queue 進行排隊称勋。因此頁面是不會呈現(xiàn)卡死狀態(tài)的。
promiseLoop 分析:點擊按鈕涯竟,產(chǎn)生一個 task赡鲜,進入 task queue 排隊。輪到它的時候庐船,執(zhí)行 promiseLoop() 方法银酬,其中 Promise.resolve() 產(chǎn)生一個 microtask 并放入 microtask queue。當 task 執(zhí)行完筐钟,接著從 microtask queue 里取出 microtask 執(zhí)行捡硅,即執(zhí)行 then(promiseLoop),它有又產(chǎn)生新的 microtask盗棵,所以 microtask queue 就一直有任務存在壮韭,因此會陷入死循環(huán),致使后面的 task纹因、更新 DOM 等永遠無法執(zhí)行喷屋。
它們會閃爍嗎?
你有沒有擔心過這些代碼會“閃”一下瞭恰?
document.body.appendChild(element)
element.style.display = 'none'
當然屯曹,實際中更多是先設置樣式再 appendChild,但效果是一樣的。
請問點擊按鈕紅色塊會閃爍嗎恶耽?
<div id="box" style="width: 100px; height: 100px; background: red"></div>
<button id="btn">Click me</button>
<script>
const btn = document.getElementById('btn')
const box = document.getElementById('box')
btn.addEventListener('click', () => {
box.style.display = 'none'
box.style.display = 'block'
box.style.display = 'none'
box.style.display = 'block'
box.style.display = 'none'
box.style.display = 'block'
// ...
})
</script>
答案:都不會密任。
分析:上述點擊事件產(chǎn)生一個 task(事件回調(diào)),只有執(zhí)行完 task 里面的代碼偷俭,才會執(zhí)行后面的微任務或更新 DOM浪讳。也就是說渲染之前,實際只有最后一行的樣式設置是起作用的涌萤,不管中間設了多少遍淹遵,瀏覽器只關心最后的樣式如何。
它們的執(zhí)行順序是负溪?
以下示例透揣,一個按鈕綁定了兩個 click 事件:
<button id="btn">Click me</button>
<script>
const btn = document.getElementById('btn')
btn.addEventListener('click', () => {
Promise.resolve().then(() => console.log('microtask 1'))
console.log('listener 1')
})
btn.addEventListener('click', () => {
Promise.resolve().then(() => console.log('microtask 2'))
console.log('listener 2')
})
// btn.click()
</script>
現(xiàn)觸發(fā) click 事件的方式有兩種:一個是通過鼠標點擊觸發(fā),另一個是通過 btn.click()
觸發(fā)川抡。這兩種方式的執(zhí)行順序一樣嗎辐真?
通過鼠標點擊的結(jié)果是:
listener 1
microtask 1
listener 2
microtask 2
通過 btn.click()
的結(jié)果是:
listener 1
listener 2
microtask 1
microtask 2
原因分析:通過與用戶交互而觸發(fā)的事件,其監(jiān)聽器是異步調(diào)用的崖堤,而通過 btn.click()
觸發(fā)侍咱,會同步派發(fā)事件,并以合適的順序同步地調(diào)用監(jiān)聽器倘感。
對于“鼠標”點擊:由于 btn 注冊了兩個 click 監(jiān)聽器放坏,鼠標點擊一次,產(chǎn)生兩個 task 進入 task queue老玛,先后執(zhí)行淤年,因此得到前面的結(jié)果。
對于 btn.click()
模擬點擊:當執(zhí)行到 btn.click()
時蜡豹,按順序同步執(zhí)行兩個監(jiān)聽器麸粮。
The
dispatchEvent()
method of the EventTarget sends an Event to the object, (synchronously) invoking the affected event listeners in the appropriate order. The normal event processing rules (including the capturing and optional bubbling phase) also apply to events dispatched manually withdispatchEvent()
.
- 執(zhí)行到第一個監(jiān)聽器 Promise.resolve() 時產(chǎn)生一個 microtask 入隊到 microtask queue。
- 接著打印 listener 1镜廉。
- 注意弄诲,此時調(diào)用棧里還沒執(zhí)行完,所以接著并不是立馬執(zhí)行 microtask queue 里的任務娇唯。而是接著執(zhí)行第二個監(jiān)聽器齐遵。同樣地,它又產(chǎn)生一個 microtask 入隊到 microtask queue塔插。
- 接著打印 listener 2梗摇。
- 此時調(diào)用棧空了想许,接著從 microtask queue 取出任務伶授,逐個執(zhí)行断序,因此先后打印 microtask 1、microtask 2糜烹。
可以通過
event.isTrusted
來區(qū)分兩種觸發(fā)方式违诗,用戶與瀏覽器交互而產(chǎn)生的事件isTrusted
為true
,使用 JavaScript 來模擬點擊等事件觸發(fā)的isTrusted
為false
疮蹦。