我正在学习执行栈、任务队列和事件循环机制。
我连续单击按钮,直到主线程可用(就在函数 a() 完成之前),如下所示。
我以为click(UI)事件和setTimeout使用同一个队列,称为Macrotask或Task Queue,所以当我在1s和3s之间点击按钮时,我认为task2的日志是在task1和task2之间打印的。但结果却并非如此。 Task2(点击事件)总是先打印,setTimeout 事件(task1,task3)在点击事件之后打印。
所以我想知道单击事件是否使用与 setTimeout 不同的队列机制,或者单击事件优先于 setTimeout。
提前感谢您的帮助
fn a done
task2 (click) done
task1 (setTimeout 1000ms) done
task2 (click) done
task3 (setTimeout 3000ms) done
task2 (click) done
fn a done
task2 (click) done
task2 (click) done
task2 (click) done
task1 (setTimeout 1000ms) done
task3 (setTimeout 3000ms) done
const btn = document.querySelector('button');
btn.addEventListener('click', function task2() {
console.log('task2 (click) done');
});
function a() {
setTimeout(function task1() {
console.log('task1 (setTimeout 1000ms) done');
}, 1000);
setTimeout(function task3() {
console.log('task3 (setTimeout 3000ms) done');
}, 3000);
// hold main thread for 6000ms(*1)
const startTime = new Date();
while (new Date() - startTime < 6000);
console.log('fn a done');
}
a();
<button>button</button>
<script src="main.js"></script>
我认为 click(UI) 事件和 setTimeout 使用相同的队列
他们没有。
UI 事件使用 用户交互 (UI) 任务源,在大多数浏览器中都有自己的 任务队列,
setTimeout
使用定时器任务源,它也有自己的 任务队列 在大多数浏览器中。
虽然规范没有要求,但 UI 任务源在几乎所有浏览器中具有最高优先级之一,而计时器具有最低优先级之一。
这种优先级的工作原理是,在事件循环处理模型的第一步,用户代理(UA)必须选择其要执行的任务的事件队列之一。
注意: 通俗地称为 “宏任务” 是任何不是 微任务的任务。
微任务队列不是任务队列,虽然在事件循环处理的第一步中可以选择微任务作为主任务,但微任务队列无法确定优先级,因为它必须被清空同步地在每个微任务检查点,这可能在每个事件循环迭代期间发生多次,特别是每次JS调用堆栈被清空时。
所以在这里,当
while
循环完成阻塞事件循环并且下一次迭代开始时,UA 将必须从哪个任务队列中选择它应该选择下一个任务。
它将在其UI任务队列中看到有新事件在等待,并执行它们,因为它们具有更高的优先级。还请注意,他们有一个饥饿系统,可以防止高优先级任务队列阻塞其他任务队列太长时间。
最后,我应该提一下,有一个建议让我们的网络开发人员直接处理所有这些优先级问题:主线程调度。
使用这个实验性功能,我们可以将代码片段重写为
if( !("scheduler" in window) ) {
console.error("Your browser doesn't support the postTask API");
console.error("Try enabling the Experimental Web Platform features in chrome://flags");
}
else {
scheduler.postTask(() => {
console.log('task1 (background) done');
}, { priority: "background" } );
scheduler.postTask(() => {
console.log('task2 (background) done');
}, { priority: "background" } );
// hold main thread for 6000ms(*1)
const startTime = new Date();
while (new Date() - startTime < 2000);
scheduler.postTask(() => {
console.log('task3 (user-blocking) done');
}, { priority: "user-blocking" } );
console.log('synchronous done');
}
并且看到最终任务首先被执行。
让我们看看你用 setTimeout 和 while 循环做了什么。
如果运行代码片段,您将看到 1 秒和 3 秒超时的时间戳基本相同。这是因为 while 循环会阻塞主线程 6 秒,直到可以运行 setTimeout 中的函数。
回调不会在 1 秒或 3 秒后执行,而是仅在主线程空闲时执行。 setTimeout 保证回调函数在至少 x 毫秒后执行,对于任务 1 来说 >= 1 秒,对于任务 3 来说 >= 3 秒。
function a() {
setTimeout(function task1() {
console.log('task1 (setTimeout 1000ms) done ' + new Date());
}, 1000);
setTimeout(function task3() {
console.log('task3 (setTimeout 3000ms) done ' + new Date());
}, 3000);
// hold main thread for 6000ms(*1)
const startTime = new Date();
while (new Date() - startTime < 6000);
console.log('fn a done');
}
a();