【问题标题】:JavaScript onMouseDown and onClick events versus event queueJavaScript onMouseDown 和 onClick 事件与事件队列
【发布时间】:2021-11-18 08:17:38
【问题描述】:

我有以下简单的 JS 代码 (https://stackblitz.com/edit/web-platform-ueq5aq?file=script.js):

const baton = document.querySelector('button');

baton.addEventListener('mousedown', (e) => {
  console.log('baton');
  baton.addEventListener('click', (e) => {
    console.log('baton click');
  });
});

当我单击一个按钮时,我会将“指挥棒”和“指挥棒点击”记录到控制台。现在我的问题是这里到底发生了什么?据我了解,在执行脚本的那一刻,处理程序mousedown 被添加到偶数队列中。当我实际单击按钮时,此处理程序将运行,因此它从事件队列中获取,添加到调用堆栈并执行。当它被执行时,处理程序“click”被添加到事件队列中。

onMouseDown 之后实际上如何触发事件onClick?这与事件队列有什么关系?为什么onMouseDown 处理程序在click 事件发生之前运行?我问是因为我有很多更复杂的代码,在不同的场景中结果是不同的。

当用户导航到包含类似脚本的 SPA 页面,然后单击按钮“接力棒”时,顺序为:

mousedown 事件 -> 处理程序 mousedown -> 处理程序单击 -> 单击事件

当用户重新加载页面时,SPA 会直接加载到该页面上,然后单击“接力棒”按钮的顺序是:

mousedown 事件 -> click 事件 -> 处理 mousedown

我正在寻找答案和真相。任何帮助将不胜感激。

附言。不幸的是,我无法在示例存储库中重现此错误 - 它发生在相当复杂的 Web 应用程序中,由于显而易见的原因,我无法在此处共享生产代码。

Ps2。只是为了澄清一下,因为可能没有足够清楚地说明:我不是在问“为什么在单击事件之前触发 mousedown 事件”,而是在问“为什么在单击事件之前运行 mousedown HANDLER”。这并不明显,因为处理程序不会立即运行。处理程序的运行顺序,首先要等待调用栈为空,这样事件队列才能被JS引擎处理。

【问题讨论】:

  • 请注意,但您的 stackblitz 链接不起作用(可能是私有的?)

标签: javascript mouseevent eventqueue


【解决方案1】:

浏览器会跟踪您单击鼠标的元素。然后它会跟踪您抬起鼠标按钮的元素。如果您解除鼠标按钮的元素是相同元素或目标元素的子元素。然后一个 click 事件被分派到你抬起鼠标的最后一个元素。然后该事件将元素链向上传播到每个父元素,除非通知该事件停止传播。

如果您在元素 A 上单击并在元素 B 上单击鼠标。那么 A 会得到鼠标按下事件,B 会得到鼠标按下事件,但都没有得到单击事件。如果您将浏览器导航到鼠标向下和鼠标向上之间的另一个页面,也会出现同样的情况。

【讨论】:

  • 在我的示例中,我总是单击 baton 元素,并且 mousedown 和 mouseup(并单击)都发生在同一个元素上并且总是在同一个页面上 - 它是 SPA 的页面 B(直接加载)或页面B 通过从页面 A 导航加载。但是在这两种情况下,结果是不同的。
  • @Furman 你没有很好地解释你的情况。您要么跳过某些内容,要么没有使用正确的词。如果您可以提供问题的工作样本,这将有所帮助。您帖子中的链接已失效。
  • 对不起,我试图尽可能清楚,但也许再一次:如果用户直接加载页面 example.com/test 并且他单击按钮“接力棒”命令是“mousedown 事件 - > 单击事件 -> 处理程序 mousedown”。当用户从example.com/other 导航到页面example.com/test 时(注意这是SPA,因此不会重新加载页面)顺序不同:“mousedown 事件-> 单击事件-> 处理程序mousedown”。 mousedown 和 click 之间的导航不是这种情况。
  • 在从“/example”导航到“/test”的情况下,文档和其他元素附加了更多处理程序,这就是我怀疑并询问事件队列的原因。我怀疑这里发生了一些竞争情况,但我需要了解这里发生的事情的细节以确认这一点。
【解决方案2】:

来自MDN Web Docs

当指针位于元素内时,同时按下和释放指针设备按钮(例如鼠标的主鼠标按钮)时,元素会接收到单击事件。

所以有一个mouseup 事件,然后是click 事件。


问题编辑后编辑:

“为什么 mousedown HANDLER 在点击事件之前运行?”

您已经在执行的mousedown 处理程序注册了click 处理程序,那么click 处理程序应该如何在它之前运行呢?

在所有先前的mousedown 处理程序中注册的所有click 处理程序也将在mousedownmouseup 事件之后运行。

【讨论】:

  • 是的,这很明显,但这不是我想要的,它没有解释我给出的示例
  • 在 mouseup 事件之后已经注册了一个点击处理程序。但我想即使你在 mouseup 中注册点击,它也会起作用
  • 但是 mousedown 处理程序必须通过事件队列,它不会立即发生。我的问题是为什么实际上它发生在click 事件之前?为什么在某些情况下(如我所述)它发生在click 事件之后?我想事件队列的大小在这里可能很重要,但我要求详细解释以了解“幕后”实际发生的情况。
  • 在 mousedown 之前点击永远不会发生(正如我在回答中引用的那样)。 mousedown 和 mouseup 起源于硬件,click 是基于一对向下和向上的“虚拟”鼠标事件
  • @Furman 我在上面的评论中的意思是,处理程序将始终首先被触发,因为 mousedown 事件必须在单击事件之前发生。从 mousedown 到 click 的人为时间是事件队列为空的方式绰绰有余,这意味着 mousedown(及其处理程序)将始终在单击(及其处理程序)之前发生(除非涉及某些机器人)..如果这不是您问题的答案..你肯定没能提出正确的问题;-;
【解决方案3】:

也许我们应该从澄清一些事情开始。

浏览器中的事件,更像是一个“嵌套层次结构”,然后是一个队列——它的工作原理被称为事件冒泡——[维基百科][1]

但是,本质上,当添加一个 EventListener 时,您正在做的事情是连接到 DOM 的一个或多个点,然后说,嘿,当 X 事件经过这里时,使用函数 Y 处理它,然后再将它向上传递堆栈。

一旦“添加”了 EventListener,它就会保持活动状态,等待获得事件。它的具体作用在其处理函数中定义。

let myYFunction = function( e ) { ... }

let myXListener = baton.addEventListern('X event', myYFunction );

// at this point, anytime 'X event' happens to baton, myYFunction will 
// be called to handle it...

现在让我们看看你的例子,让我们把事情分解一下,

const baton = document.querySelector('button');

第一行,只是查询 DOM,在页面中找到第一个类型为“按钮”的元素。对...这是我们要插入事件处理程序的“位置”。我们可以将它们添加到任何元素,在 DOM 中的任何位置,如果我们愿意,我们甚至可以挂钩到“body”元素。

好的,那你有这个了,

baton.addEventListener('mousedown', (e) => {
  console.log('baton');
  baton.addEventListener('click', (e) => {
    console.log('baton click');
  });
});

这是“嵌套”“click”事件侦听器的创建,但仅在“处理”“mousedown”事件之后。没有真正的理由必须在 mousedown 处理程序的函数体中注册 'click' 事件。

如果我们稍微重写一下,可能会更清楚实际发生了什么。

baton.addEventListener('mousedown', (e) => {
  console.log('baton mousedown');
}

baton.addEventListener('click', (e) => {
    console.log('baton click');
});

此外,我还要指出,目前它的工作方式“有效”——但它实际上隐藏了一点草率的编码......你每次触发“mousedown”事件时都会看到一个新的' click' eventListener 正在注册...所以最终您可能会得到很多很多点击处理程序来响应单个 'click' 事件...查看 MDN 以了解有关 [this][2]

的更多信息>

我希望这能回答您最初关于发生了什么的问题。


对于您的问题“当我单击一个按钮时,我会在控制台中记录 'baton' 和 'baton click'。现在我的问题是这里到底发生了什么?” -- 对我来说,它看起来像这样:

  1. 添加了“mousedown”事件监听器,但没有“执行”
  2. 'mousedown' 事件发生,现在您的 'mousedown' 侦听器执行它的函数,该函数又注销到控制台,并注册一个新的 'click' 处理程序 - 但同样没有执行。

接下来,对于指挥棒看到的每个“鼠标按下”,都会重复第 1 步和第 2 步。此外,对于通过接力棒传递的任何“点击”事件——在接力棒上的每次“鼠标按下”之后都会发生:

  1. 发生“点击”事件,然后执行“点击”处理程序并注销到控制台。

SPA 事件处理策略

在使用 SPA 时,显示多个“页面”,在同一个页面加载中......它可能会变得一团糟,所有这些事件监听器都在周围堆积在一起。如果您打算在 SPA 的“页面”之间使用 eventListeners,您可能还想研究如何“删除”它们。 - [MDN][3]

这样,您的 SPA 的当前“页面”只有活动监听器。

另外,考虑“泛化”您的处理程序,并将它们附加到 DOM 中更高的位置...这将允许您只有少数事件侦听器将事件“路由”到它们的“逻辑”处理程序。


随机/不同的行为

通过上面概述的步骤,1、2 和 3 以及它们如何不会同时发生。您将看到似乎是随机输出到控制台的内容...尝试运行类似的内容,以获得正确的感觉:

let cCount = 0;
let mCount = 0;
let tCount = 0;

const baton = document.querySelector('button');

baton.addEventListener('mousedown', (e) => {
  console.log('mousedown # ' + (mCount++) + ' order:' + tCount++);
  baton.addEventListener('click', (e) => {
    console.log('click # ' + (cCount++) + ' order:' + tCount++);
  });
});

[1]:https://en.wikipedia.org/wiki/Event_bubbling#:~:text=Event%20bubbling%20is%20a%20type,Provided%20the%20handler%20is%20initialized)。 [2]:https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener [3]:https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/removeEventListener

【讨论】:

  • 这是质量和详细的答案,感谢您的努力,但我不能同意某些观点。关于第一部分: baton.addEventListener('mousedown', (e) => { console.log('baton'); baton.addEventListener('click', (e) => { console.log('baton click') ; }); });绝对不等于一个接一个地分别附加两个处理程序(正如您所写的“如果我们重新编写它,可能会更清楚实际发生的情况。”在我的示例中,我写了 click 处理程序在 mousedown 处理程序之后有条件地附加运行。如果在 mousedown 处理程序中我们分离点击事件监听器
  • 它永远不会发生。这就是我的“生产”代码中的情况。关于“发生'mousedown'事件,现在你的'mousedown'监听器执行其功能”部分。我这里也不能同意。当 mousedown 事件发生时,mousedown 监听器不会立即执行。一旦调用堆栈为空,并且事件队列中的先前句柄已被执行,它将立即执行。
  • 关于您的“处理 SPA”部分 - 是的,我知道删除未使用的事件侦听器和内存泄漏,我都知道这一点。但在实际情况下,这并不是那么简单。当某些侦听器来自外部库(例如哨兵)而无法分离时,这是一个复杂的应用程序。
  • 总体上感谢您的好帖子,但恐怕它不能解释我的问题。我需要了解“为什么 mousedown HANDLER 在(取决于情况)单击事件之前/之后运行”。点击事件如何触发与等待在事件队列中运行的处理程序相关。我无法在任何地方找到解释,并且这个问题的答案(尽管我毫不怀疑作者尽最大努力提供最佳和详细的解释)也没有解释它。
  • 至于为什么有时前有时后是非阻塞异步函数,如果你需要保证它们的执行顺序,你就需要使用额外的状态和内务策略。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2018-09-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多