你确定你知道事件在 JavaScript 中是如何传播的吗?
事件在 Web 编程中无处不在——输入变化、鼠标移动、按钮点击和页面滚动都是事件。这些操作由系统生成,您可以通过注册事件监听器以任何您喜欢的方式响应它们。
这为用户带来了交互式体验。了解现代 Web 浏览器中事件模型的工作原理,可以帮助您构建强大的 UI 交互。如果处理不当,就会出现 bug。
本文旨在阐述 W3C 事件模型中事件传播机制的一些基础知识。所有现代浏览器都实现了该模型。
让我们开始吧⏰。
事件传播
想象一下,如果我们有两个 HTML 元素,element1和element2,其中element2是element1的子元素,如下图所示:
我们给它们都添加点击处理程序,如下所示:
element1.addEventListener('click', () => console.log('element1 is clicked'));
element2.addEventListener('click', () => console.log('element2 is clicked'));
你认为点击element2时会输出什么?🤔
答案是element2 is clicked
,其次是element1 is clicked
。这种现象被称为事件冒泡,它是W3C事件模型的核心部分。
在事件冒泡中,最内层的目标元素首先处理事件,然后在 DOM 树中向上冒泡,寻找具有注册事件处理程序的其他祖先元素。
💡 在事件冒泡中,最内层的目标元素首先处理事件,然后它在 DOM 树中冒泡
现在,有趣的是,事件流并非像你可能认为的那样是单向的。W3C 事件模型中的事件流机制是双向的。惊喜!😯。
在使用 React 等框架时,我们主要处理事件冒泡,而从未考虑过另一个阶段,即事件捕获。
💡 事件冒泡只是硬币的一面;事件捕获是另一面。
在事件捕获阶段,事件首先被捕获,直到到达目标元素(event.target
)。作为 Web 开发人员,您可以在此阶段通过将其设置为方法true
中的第三个参数来注册事件处理程序addEventListener
。
// With addEventListener() method, you can specify the event phase by using `useCapture` parameter.
addEventListener(event, handler, useCapture);
默认情况下,它为false,表示我们在冒泡阶段注册此事件。
让我们修改一下上面的示例,以便更好地理解这一点。
// Setting "true" as the last argument to `addEventListener` will register the event handler in the capturing phase.
element1.addEventListener('click', () => console.log('element1 is clicked'), true);
// Whereas, omitting or setting "false" would register the event handler in the bubbing phase.
element2.addEventListener('click', () => console.log('element2 is clicked')));
我们添加了true
foruseCapture
参数,表示我们将在捕获阶段为element1注册事件处理程序。对于element2,省略或传递该参数false
都将在冒泡阶段注册事件处理程序。
现在,如果你点击element2,你会看到element1 is clicked
先打印 ,然后打印element2 is clicked
。这就是捕获阶段的实际操作。
💡 在事件捕获阶段,首先捕获事件,直到到达目标元素
下面的图表可以帮助您轻松地将其形象化:
事件流顺序为:
- “click”事件在捕获阶段开始。它会查找element2的任何祖先元素是否有
onClick
捕获阶段的事件处理程序。 - 该事件找到元素1,并调用处理程序,打印出
element1 is clicked
。 - 事件向下流向目标元素本身(element2),并寻找路径上的任何其他元素。但没有找到捕获阶段的事件处理程序。
- 到达element2后,冒泡阶段开始并执行在element2上注册的事件处理程序,打印
element2 is clicked
。 - 事件再次向上传播,寻找目标元素(element2)的祖先元素,该祖先元素具有冒泡阶段的事件处理程序。然而,目标元素的祖先元素并没有找到,所以什么也没有发生。
所以,这里要记住的关键点是,整个事件流是事件捕获阶段和事件冒泡阶段的组合。作为事件处理程序的作者,您可以指定在哪个阶段注册事件处理程序。🧐
掌握了这些新知识后,我们来回顾一下第一个例子,并尝试分析为什么输出是倒序的。这里再次引用第一个例子,这样你就不会制造scroll
事件了 😛
element1.addEventListener('click', () => console.log('element1 is clicked'));
element2.addEventListener('click', () => console.log('element2 is clicked'));
省略该useCapture
值会在冒泡阶段为两个元素注册事件处理程序。点击element2 时,事件流顺序如下:
- "click" 事件在捕获阶段开始。它会查找 element2 的任何祖先元素是否有用于
onClick
捕获阶段的事件处理程序,但未找到。 - 事件向下传播到目标元素本身(element2)。到达 element2 后,冒泡阶段开始,执行在 element2 上注册的事件处理程序,并打印
element2 is clicked
。 - 事件再次向上传播,寻找目标元素(element2)的任何祖先,该祖先具有冒泡阶段的事件处理程序。
- 此事件在element1上找到一个。处理程序被执行并
element1 is clicked
打印出来。
另一件有趣的事情是注销事件的eventPhase属性。这有助于你直观地了解当前正在执行事件的哪个阶段。
element1.addEventListener("click", (event) =>
console.log("element1 is clicked", { eventPhase: event.eventPhase })
);
如果您想试用,可以参考Codepen 上的演示。或者,您也可以将下面的代码片段粘贴到浏览器中亲自查看。
const element1 = document.createElement("div");
const element2 = document.createElement("div");
// element1: Registering event handler for the capturing phase
element1.addEventListener(
"click",
() => console.log("element1 is clicked"),
true
);
// element2: Registering event handler for the bubbling phase
element2.addEventListener("click", () => console.log("element2 is clicked"));
element1.appendChild(element2);
// clicking the element2
element2.click();
停止事件传播
如果您希望在任何阶段阻止当前事件的进一步传播,您可以调用对象上可用的stopPropagation方法Event
。
因此,这意味着在元素1的event.stopPropagation()
事件处理程序中(在捕获阶段)调用它,将会停止传播。即使你现在点击元素2,它也不会调用它的处理程序。
以下示例表明:
// Preventing the propagation of the current event inside the handler
element1.addEventListener(
"click",
(event) => {
event.stopPropagation();
console.log("element1 is clicked");
},
true
);
// The event handler for the element2 will not be invoked.
element2.addEventListener('click', () => console.log('element2 is clicked'));
请注意,这event.stopPropagation
仅会停止传播。但它不会阻止任何默认行为的发生。例如,点击链接仍会被处理。要停止这些行为,您可以使用event.preventDefault()
方法。
最后,这里还有另一个很酷的JSbin 演示,如果您愿意尝试一下并了解如何通过 停止事件传播event.stopPropagation
。
希望这篇文章对你有所帮助,并能给你一些启发。感谢阅读😍
有用的资源:
- “DOM 事件”简介- (whatwg 规范)
- 事件介绍- (Mozilla 文档)
- 事件阶段和停止传播演示- (JSbin 演示)