stopImmediatePropagation

JavaScript

在 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

几点说明:

  1. React 的合成事件只有 stopPropagation 没有 stopImmediatePropagation,如果需要使用的话,可以用如下的方法调用真正的 DOM API:.nativeEvent.stopImmediatePropagation。这里 React 不需要 stopImmediatePropagation 的理由非常简单,因为在 JSX 中,每个事件在 Component 上只能绑定一个回调函数,因此 stopImmediatePropagation 是多余的;
  2. 由于浏览器天然维护了一个 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 元素一个一个的隐藏。

  1. stopImmediatePropagation API 的浏览器支持比较好,在 IE 9 及以上的浏览器中都可以使用,参考 Can I Use
  2. 更多关于这个 API 的介绍,可以参考 MDN