事件处理程序即使在事件发生之后定义,仍会被执行。

3

Consider the following code:

function something(){
  ...
  document.addEventListener('click', clickHandler);
}

function clickHandler(e) {...}

当单击页面内部存在的图标时,会调用something()。因此,单击该图标时,clickHandler()也会被执行。这完全无法解释,因为事件监听器是在之后添加的。 有什么好的想法吗?


1
如果您再次点击图标,clickHandler()会执行两次吗? - Klaycon
1
是的。它已经执行了。 - Unknown developer
1
事件在DOM中上下冒泡。当你在document上注册一个监听器时,无论你点击哪个元素,它都会被调用。 - Dan O
1
@DanO 事件只会向上冒泡,而被捕获的位置在下方。 ;) - Scott Marcus
@DanO 另外,这并没有解释为什么处理程序会被调用两次(除非在图标上还设置了另一个处理程序)。冒泡有助于事件绑定,但它不会影响事件触发的次数。 - Scott Marcus
显示剩余2条评论
2个回答

3
你的答案在问题的第一行:
“单击页面内存在的图标时,会调用something()。”
所以,你必须已经将something设置为图标的click处理程序,以便该行为能够正常工作。这意味着每次单击图标时,都会触发由something处理的click事件,但是你还会为document注册完全不同的click事件处理程序。
由于事件会冒泡,因此对图标的任何点击(位于document中)都会触发该元素的click事件处理程序(something),然后事件会冒泡到document,它将触发其自己的click事件处理程序(clickHandler)。
因此,这里没有什么是错序的。这只是一个简单的情况,其中一个事件处理程序设置另一个事件处理程序,并且由于第一个处理程序在第二个处理程序接收事件之前被调用,因此第二个绑定在时间上可以使document处理冒泡事件。
这是您的代码(稍作修改以便运行)和注释:

document.querySelector("div").addEventListener("click", something);

function something(){
  console.log("function something has been invoked because the div was clicked");
  
  // Every time you click the div, you initiate a click event WITHIN the document
  // that will propogate upward to the document itself and the next line sets up
  // a handler at that level:
  document.addEventListener('click', clickHandler);
}

function clickHandler(e) {
  console.log(e.type + " triggered by: " + e.target + " handled at the document level");
}
div { background-color:#800080; color:orange;}
<p>First, click anywhere outside of the purple div and nothing will happen because the only thing
   that initially has a click event set up for it is the div. Then click on the div and you'll
   not only get the div's event handler to run, but because that handler sets up a click event
   handler for the document and the event hasn't bubbled up there yet, you'll get that same
   click event handled again, but by the document.
</p>
<p>
  Then, click outside of the div again. Because the document now has an event handler set up
  for it, the click will be handled there.
</p>
<div>Click me.</div>

如果这种行为不是您想要的,那么您有几个选择:
您可以使用event.stopPropagation()停止事件进一步冒泡(从而防止它到达文档):

document.querySelector("div").addEventListener("click", something);

function something(e){
  // Handle the event here but don't allow it to go anywhere else
  e.stopPropagation();
  console.log("function something has been invoked because the div was clicked");
  
  // Every time you click the div, you initiate a click event WITHIN the document
  // that will propogate upward to the document itself and the next line sets up
  // a handler at that level:
  document.addEventListener('click', clickHandler);
}

function clickHandler(e) {
  console.log(e.type + " triggered by: " + e.target + " handled at the document level");
}
div { background-color:#800080; color:orange;}
<p>First, click anywhere outside of the purple div and nothing will happen because the only thing
   that initially has a click event set up for it is the div. Then click on the div and you'll
   not only get the div's event handler to run, but because that handler sets up a click event
   handler for the document and the event hasn't bubbled up there yet, you'll get that same
   click event handled again, but by the document.
</p>
<p>
  Then, click outside of the div again. Because the document now has an event handler set up
  for it, the click will be handled there.
</p>
<div>Click me.</div>

在文档级别处理所有点击事件,只需让所有事件冒泡到那里,然后通过确定哪个元素首先触发了事件来响应事件。这被称为"事件委托"。使用event.target可以访问该元素:

// All clicks will eventually bubble up to the document
document.addEventListener('click', clickHandler);

function clickHandler(e) {
  console.log(e.type + " triggered by: " + e.target + " handled at the document level");
  
  // You can handle the event differently based on which element triggered it.
  if(e.target.classList.contains("notSpecial")){
    e.target.classList.add("special");
  }
}
.special { background-color:red; }
.as-console-wrapper { max-height: 1.5em !important; }
<h1>click me</h1>
<p>No, click me!</p>
<div>What about me?!</div>
<div class="notSpecial">Click me. I'm special</div>


0

当你添加了这个处理程序后,似乎当前事件的处理程序仍然会被调用,设置useCapture值为true似乎可以阻止它。

function something(){
  ...
  document.addEventListener('click', clickHandler, true);
}
useCapture 可选参数 一个布尔值,指示此类型的事件是否会在分派给 DOM 树中位于其下方的任何 EventTarget 之前分派给已注册的侦听器。通过树向上冒泡的事件不会触发使用捕获的侦听器。事件冒泡和捕获是在一个元素嵌套在另一个元素内部时,当这两个元素都为该事件注册了处理程序时,传播事件的两种方式。事件传播模式确定元素接收事件的顺序。有关详细说明,请参阅 DOM Level 3 Events 和 JavaScript Event order。如果未指定,则 useCapture 默认为 false。

如果你愿意的话,你还可以使用stopPropagation()来明确地阻止事件继续冒泡。在添加事件监听器之后(无论你需要在哪里添加它...),你可以防止当前事件触发它。 - Mike Robinson
由于并非所有事件都可以在捕获阶段处理,并且因为IE早期不支持它,所以它并不被广泛使用或非常有用。stopPropagation()可以解决这个问题。但实际上,你的回答并没有解释为什么会出现这种情况。 - Scott Marcus
嗯,"为什么会发生这种情况"已经很明显了。代码在一个事件触发的地方添加了一个事件处理程序,该事件会继续通过可能对其感兴趣的每一段代码进行传播,并触发刚刚添加的处理程序,因为它被添加到尚未被事件传播遍历器访问过的对象上。 - Mike Robinson
@MikeRobinson 对你和我来说可能很清楚,但问题被提出意味着对于提问者和其他人来说并不清楚。因此,任何“答案”都应该解答这个问题。 - Scott Marcus

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