之前写过一篇 浏览器事件循环的文章。今天又忽然对 dom 事件这个点有点疑惑,于是做实验重新写一盘。
我们都知道“微任务”要比“宏任务”具有更高优先级,微任务队列要一次性清空后,才能轮得到宏任务队列的清空。于是代码中同时 Promise 状态变化的回调总是要比 setTimeout 这类事件的回调更早触发。
然而,假设 2 个事件都是宏任务事件的话,他们真的是以“谁先触发就先执行谁的回调”这样的原则来处理吗?实际测试发现宏任务中貌似也有着不同优先级的队列,例如“DOM 事件”总是要比“setTimeout 事件”要更早的触发回调。
举个例子
看下面这个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta http-equiv="X-UA-Compatible" content="ie=edge" /> <title>Document</title> </head> <body> <div> <button id="start" onclick="start()">开始</button> <button id="testBtn" onclick="test()">测试按钮</button> </div> <script> function start() { // 开始后,立刻推送900ms倒计时逻辑 setTimeout(function () { console.log("倒计时timer handler执行"); }, 900); // 接下来立刻卡住线程 var startTime = new Date(); while (new Date().getTime() - startTime < 5000) {} // 实验 1:在线程卡住开始后, 900 毫秒倒计时之内,你点一下test b按钮 // 实验 2:在线程卡住开始后, 900 毫秒倒计时之外,你点一下test b按钮 }
function test() { console.log("测试按钮回调函数执行"); }
// 结果分析 // 1. timer本应该 900 毫秒后就执行,结果他被卡到5 秒后再执行。 这个是因为 js 单线程机制,很容易理解。 // 2. 你在第900毫秒时候有个宏任务 setTimeout 发生,你在第3秒的时候,点了一下“测试按钮”,这个 dom 点击宏任务理论上也会推入宏队列队列中。 这俩事件都是等待 js 空闲时候调用,最终你都会发现:即使测试按钮点击的比timer 晚,他依然永远都是 “测试按钮的回调” 先执行。 // 3. 这是否说明 dom event 宏任务,要比 timer 宏任务队列具有更高优先级? </script> </body> </html>
|
运行后,界面上会有 2 个按钮:
- 当你点击开始后,js 线程会设置一个 900 毫秒定时器,并卡死线程 5 秒。
- 当你在卡死的五秒钟内,点击“测试按钮”。最后等待五秒。会发现你的 dom 事件回调永远要比 setTimeout 回调要更早。
第一次实验结论
基于此,我们初步推测:宏任务中也有队列优先级。
即“DOM 事件”的优先级要比其他宏任务队列优先级高,因此 js 空闲后会优先去清空“DOM 事件”队列。猜测可能是出于提高页面响应性的目标而做出此种决策。
那么 dom 事件跟微任务相比呢
结论是:微任务优先级要比 dom 事件更高
看实验代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| <!DOCTYPE html> <html lang="en"> <head> </head> <body> <div> <button id="start" onclick="start()">开始</button> <button id="testBtn" onclick="test()">测试按钮</button> </div> <script> function start() { Promise.resolve().then((res) => { console.log("Promnise 微任务1执行"); });
var startTime = new Date(); while (new Date().getTime() - startTime < 5000) {}
Promise.resolve().then(() => { console.log("promise 微任务 2"); }); }
function test() { console.log("测试按钮回调函数执行"); } </script> </body> </html>
|
经测试,打印顺序为:promise 微任务 1—-promise 微任务 2—-测试按钮点击。
也就是说,即使微任务注册的比你 click 时间晚,微任务队列依然要先清空后,才能轮到执行你的 click 回调。
至于原因:如果我们认为“dom event”就是宏任务,那么该实验结果是符合预期的。毕竟 js 线程空闲后,要先把之前注册过的微任务清空,再执行宏任务。这是我们过往从互联网上学习过的经验结论。
那么 dom 事件到底是什么任务?
- 由上述 Promise 的对比实验可知,dom event 属于宏任务
- 由上文最开始的 setTimeout 实验对比可知,dom Event 比 setTimeout 更早触发,所以 dom 事件又像是微任务。
综合结论:dom event 我们可以认为是一种 “比其他宏任务优先级更高”的宏任务。这或许是浏览器为了提高响应流畅度而设计的策略。
其他
注意:大家做 dom event 实验时,要避免使用编程方式触发 event(例如 dispatchEvent 方式),而要自己用鼠标去点。 因为 dispatchEvent 触发 dom 事件是同步的,同步的触发肯定永远比任何宏任务微任务都要快。 关于 dispatchEvent 这种方式触发的 dom 事件是同步的原理,在这篇知乎有讲。