微任务中的事件是否会冒泡?

4

点击后,这将输出 1 然后是 2:

const myElem = document.getElementById("myElem")
myElem.addEventListener('click', () => queueMicrotask(() => console.log(1)));
window.addEventListener('click', () => console.log(2));
<button id="myElem">Click me</button>

如果将queueMicrotask替换为setTimeout,输出结果为2然后是1。这意味着冒泡发生在微任务中,而不是宏任务中。
这是DOM规范的一部分还是仅限于浏览器实现细节?

1
不确定你是如何得出那个结论的。在queueMicrotask的位置调用setTimeout并不会改变事件相关的任何内容。 - Ouroborus
“这意味着微任务中存在冒泡,而不是说冒泡中存在微任务检查点。是的,这是DOM规范的一部分。@Kaido可能会告诉我们确切的位置。” - Bergi
1
@Bergi,通常情况下是通过 https://webidl.spec.whatwg.org/#call-a-user-objects-operation 从 https://dom.spec.whatwg.org/#concept-event-listener-invoke 调用 https://html.spec.whatwg.org/multipage/webappapis.html#calling-scripts:clean-up-after-running-script 进行清理。 - Kaiido
1
浏览器并不是用JS编写的。浏览器实际上忽略了JS微任务,它们确实遵循HTML事件循环处理模型,但幸运的是,ES在编写时查看了当时HTML的处理方式,因此它实际上是兼容的。所以,是的,操作系统向浏览器发送设备说的话的消息,浏览器排队执行任务,稍后它将选择执行该任务,该任务将调度事件,并在此过程中执行JS回调和微任务检查点。 - Kaiido
@Kaiido 谢谢,现在我明白了。你想写一个答案让我接受吗? - Alexey Berezkin
显示剩余6条评论
2个回答

3
在微任务队列中,事件冒泡只会在以下两种情况下发生:从微任务任务队列内部运行的代码中将自定义事件分派到元素上,或者使用“slotchange”事件内部信号通知 DOM 已被修改(使用变异观察器)。在这两种情况下,事件是从正在执行的微任务中触发的,并且 事件冒泡在 JavaScript 执行线程中同步发生。相比之下,“本机”事件是由浏览器触发的,并通过事件循环异步调用事件处理程序
在发布的事件场景中缺少的是,如果为同一类型的本机事件添加了多个事件处理程序,则这些处理程序将从事件循环中调用。因此,在控制从处理程序调用返回到事件队列之前,任何处理程序中排队的微任务都将被执行。 以下是针对两个“click”处理程序的事件流演示:

"use strict";
document.querySelector("div").addEventListener("click", click1);
window.addEventListener("click", click2 /*, {capture:true}*/ );

function click1(event) {
   console.log("click1 called for div")
   queueMicrotask( ()=> console.log("microtask1 executes"));
   console.log("click1 exits");
}

function click2(event) {
   console.log("click2 called for window")
   queueMicrotask( ()=> console.log("microtask2 executes"));
   console.log("click2 exits");
}
div {background-color: yellow}
<div>Click this div</div>
or click the window,

运行代码片段并单击 div 以确认 div 处理程序被调用、退出并在冒泡发生后窗口处理程序被调用之前启动的微任务运行。请注意,虽然是从事件循环中调用的,但它们的调用顺序将取决于是否使用事件捕获(在片段中已注释掉)。
如果您在文章的第一个点击处理程序中用 setTimeout 替换 queueMicrotask,则回调函数不再在返回事件循环之前在微任务队列中执行,从而允许事件循环在计时器回调之前调用第二个点击处理程序(用于计时器到期之前生成的事件)。

1
@AlexBezkin 我认为处理程序是从本地浏览器代码运行并使用事件循环以干净的堆栈调用的。在W3C Dom 3事件规范下提供了一个良好的事件流图,其中包括"事件分派和DOM事件流"。执行事件捕获、冒泡、将事件移动到传播路径上同时更新Event对象属性并响应方法调用的浏览器代码的详细信息以及通过事件循环调用事件处理程序的调用不在我的专业范围内,但我个人不会称这样的代码为“宏任务”。 - traktor
@traktor 感谢你。在 MDN 上写的 "handlers are called via event loop" 这句话并不准确。实际上,JavaScript 事件循环中的微任务和宏任务并没有被使用,而是通过分派所有事件来实现类似于高级微任务的功能,其还可以运行其他微任务(检查点)。有趣的是我们都被告知要学习 JavaScript 事件循环,但在浏览器中它的工作方式却不同,这甚至在 MDN 中都没有提到。 - Alexey Berezkin
@AlexeyBerezkin 不要这么快 :-) Mutation Observer API 可以从微任务队列内派发事件,但就像在元素上分派的用户事件一样,事件分派是同步的:所有合格的事件处理程序将依次被调用,并且控制将返回到事件分派调用者,而不会首先返回到事件循环。对于本地事件来说,情况并非如此:如果个别“click”事件处理程序(比如)排队了一个微任务,则该微任务将在控件返回到事件循环之前执行,并在调用相同事件的其他处理程序之前执行。MDN 是正确但简洁的。 - traktor
1
@AlexeyBerezkin 注意网上发布的过度简化。例如,https://javascript.info/event-loop声称“队列中的任务按照“先到先服务”的原则进行处理”,但没有提到事件循环具有多个任务来源和任务队列,并且可以根据它们来自的队列优先处理作业[也请参见](https://dev59.com/LbDla4cB1Zd3GeqP5kZa)。当在浏览器中处理UI事件时,这种机制可能会发挥作用 - 或者仅仅是在处理程序调用之间执行微任务队列。HTML标准不涵盖此过程。 - traktor
@traktor 这里有没有一些好的网络教程,描述任务源和它们的优先级的真实情况?我感觉这部分完全被忽略了,只在规范中进行了描述。 - Alexey Berezkin
显示剩余4条评论

0

顺序上的差异并不是关于事件冒泡的。 这是因为 setTimeout -- 就像点击监听器本身一样 -- 使其匿名函数的执行等待(至少)一个完整的事件循环迭代。使用 queueMicrotask 的好处是避免这种额外的延迟。

这可能有助于澄清过程:

const
  betweenTasks = (msg) => queueMicrotask( ()=>console.log(msg) ),
  futureTask = (msg) => setTimeout( ()=>console.log(msg), 0),      
  myElem = document.getElementById("my-elem");

myElem.addEventListener("click", () => {
  futureTask("callbacks to `setTimeout` must wait additional iteration(s)");
  betweenTasks("microtasks run between tasks");
  console.log("callbacks to event listeners must wait in the task queue");
});

window.addEventListener("click", () => {
  console.log("bubbled callbacks happen later than direct-target callbacks");
});
<button id="my-elem">Click me</button>


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接