在 JavaScript 的 DOM 事件中,可以通过 .stopPropagation
来阻止事件冒泡。比如,如果有如下的一个 DOM 结构:
<div id=parent>
<div id=child></div>
</div>
同时有如下的 JavaScript 代码:
const parent = document.getElementById('parent');
const child = document.getElementById('child');
parent.addEventListener('click', function (event) {
console.log('click (parent): capture');
}, true);
child.addEventListener('click', function (event) {
console.log('click (child): capture');
}, true);
child.addEventListener('click', function (event) {
console.log('click (child): bubble');
}, false);
parent.addEventListener('click', function (event) {
console.log('click (parent): bubble');
}, false);
那么,点击 child 元素,console 中的输出的结果如下:
click(parent): capture
click(child): capture
click(child): bubble
click(parent): bubble
这里,代码有意保持输出顺序和回调函数注册顺序的一致性。如果在上面四个回调函数中依次加上 event.stopPropagation()
,那么之后所有的内容将不会在继续输出。
以上是关于 DOM 中冒泡和捕获事件处理的一般流程。这里,如果在一个 DOM 节点上注册了不止一个的事件回调函数,那么浏览器将按照事件注册的先后顺序,依次执行对应的回调函数。需要注意的一点是,event.stopPropagation()
是无法阻止同级回调函数被执行的。简单将上面的代码进行修改,可以得到如下的测试代码:
parent.addEventListener('click', function (event) {
event.stopPropagation();
console.log('click (parent): first capture');
}, true);
parent.addEventListener('click', function (event) {
console.log('click (parent): second capture');
}, true);
child.addEventListener('click', function (event) {
console.log('click (child): capture');
}, true);
那么,在点击 child 元素的时候,可以得到如下的输出结果:
click (parent): first capture
click (parent): second capture
parent 上的 click 回调函数都依次执行完毕了,而 child 上的部分则因为 event.stopPropagation()
没有被执行到。这里,如果希望连同层的其他回调函数也不要继续执行,可以改用 event.stopImmediatePropagation()
,代码修改如下:
parent.addEventListener('click', function (event) {
event.stopImmediatePropagation();
console.log('click (parent): first capture');
}, true);
parent.addEventListener('click', function (event) {
console.log('click (parent): second capture');
}, true);
child.addEventListener('click', function (event) {
console.log('click (child): capture');
}, true);
修改后的代码,执行效果如下:
click (parent): first capture
几点说明:
- React 的合成事件只有
stopPropagation
没有stopImmediatePropagation
,如果需要使用的话,可以用如下的方法调用真正的 DOM API:.nativeEvent.stopImmediatePropagation
。这里 React 不需要stopImmediatePropagation
的理由非常简单,因为在 JSX 中,每个事件在 Component 上只能绑定一个回调函数,因此stopImmediatePropagation
是多余的; - 由于浏览器天然维护了一个 EventListener 的队列用于按顺序执行回调函数,
stopImmediatePropagation
配合上回调函数的注销(removeEventListener
),可以用于小成本实现一个 FIFO 的队列。示例代码如下:
function register(dom) {
function callback(event) {
if (event.key !== 'Escape') return;
event.stopImmediatePropagation();
window.removeEventListener('keydown', callback, true);
dom.attributeStyleMap.set('display', 'none');
}
dom.attributeStyleMap.set('display', 'block');
window.addEventListener('keydown', callback, true);
}
Array.from(document.querySelectorAll('ul li'))
.forEach(register);
以上代码执行后,按下 ESC 键,将会依次将 ul
下的 li
元素一个一个的隐藏。