返回专题

专题 2018

React 事件传播

React 事件传播 相关笔记

Notebook
本文目录5
  1. 问题
  2. 合成事件与原生事件
  3. 原理
  4. stopPropagation 和 stopImmediatePropagation
  5. 结论

问题

我们在编写 react 业务代码的时候,基于一些需求往往需要阻止事件的传播,当页面中既有 react 的合成事件,又有浏览器的原生事件时,对于如何正确阻止事件的传播可能会产生一些疑惑,什么情况下使用 event.stopPropagation,什么情况下使用 event.stopImmediatePropagation,event.nativeEvent 下的 stopPropagation 和 stopImmediatePropagation 又是怎么回事,接下来让我们一探究竟

合成事件与原生事件

react 在绑定事件时有着自己的一套机制,并没有将事件直接绑定在 dom 上面,因为如果 dom 上绑定了过多的事件处理函数,整个页面的响应及内存占用可能会受到影响,为了避免 dom 事件的滥用,同时解决原生事件在不同浏览器的差异问题,react 将事件都委托到了 document 上,将原生事件进行额外的处理,形成自己的合成事件。

react 中绑定的事件是合成事件,我们常常在 jsx 中这么写:

JavaScript
<button onClick={this.onClick}>单击触发react事件</button>

原生事件:

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

原理

  • React 并不是将 click 事件绑在该 div 的真实 DOM 上,而是在 document 处监听所有支持的事件,当事件发生并冒泡至 document 处时,React 将事件内容封装并交由真正的处理函数运行

下面通过一个小例子来看看究竟是怎么回事,首先定义下面简单结构,并添加一些事件:

JavaScript
class EventDemo extends React.Component {
    return (
        <div className="top-level" onClick={() => {console.log('React event: top-level click!')}}>
            <div className="middle-level" onClickCapture={() => {console.log('React event: middle-level click capture!')}}>
                <div className="last-level" onClick={() => {console.log('React event: last-level click!')}}>click me!</div>
            </div>
        </div>
    )
}

其中的 onClickCapture 平时用的比较少,表示在捕获阶段执行该事件,同时在 componentDidMount 之后添加一些原生事件,以方便我们进行对比:

JavaScript
class EventDemo extends React.Component {
  componentDidMount() {
    const topLevel = document.querySelector('.top-level');
    //  true表示事件捕获阶段触发
    topLevel.addEventListener(
      'click',
      function() {
        console.log('DOM event: top level click capture!');
      },
      true
    );

    const middleLevel = document.querySelector('.middle-level');
    middleLevel.addEventListener(
      'click',
      function() {
        console.log('DOM event: middle level click!');
      },
      false
    );
  }
  // ...
}

保存完后点击 click me!按钮,控制台打印的信息如下:

控制台log

从结果可以看到 react 是先执行的绑定在元素中的原生事件,再触发 react 事件,这也是 react 将事件都统一绑定到 document 的结果。当事件触发时,先执行原生事件的捕获阶段-冒泡阶段,期间触发相应的原生事件,当事件冒泡到 document 的时候再触发 react 模拟的合成事件的捕获阶段-冒泡阶段,上述代码可以图解成下面流程:

事件传播流程

stopPropagation 和 stopImmediatePropagation

从图中可以看到合成事件基本上是在原生事件全部触发之后才会触发,所以如果在原生事件里调用 event.stopPropagation(),则会阻止原生事件冒泡到 document,那么所有的合成事件都将不能正常触发,因为 react 将事件都统一绑定到了 document 上。

而 react 事件中的 event 是对原生事件对象的封装,并不是原生 DOM 事件的 event,event.nativeEvent 才是原生事件对象,在 react 事件中调用 event.stopPropagation()可以阻止后续的 react 事件,这个应该很好理解。那么在 e.nativeEvent 中调用 stopPropagation(),能起到跟上面在原生事件里调用 stopPropagation()一样的作用吗?

答案是不能。因为如上图事件执行顺序就可以知道,这个时候原生事件已经几乎都被触发完成,成为历史,而历史是无法改变的。。。同理,event.nativeEvent.stopImmediatePropagation 也是一样的,不过 event.nativeEvent.stopImmediatePropagation 可以阻止绑定在 document 上冒泡阶段的原生事件,因为该事件与其它原生事件不同,是在 react 合成事件触发完成后才触发(为减少混淆,上面流程图中未体现)

原生事件可以对同个元素绑定多个同种类型的事件,event.stopImmediatePropagation 被调用时可以阻止该元素后续事件的触发执行,而 react 体系中,一个元素只能绑定一个同类型的事件监听器,重复定义时,前面的监听器会被后面的覆盖,所以 react 事件中并没有封装 stopImmediatePropagation。

结论

结论 清楚了 react 中合成事件与原生事件的触发流程后,在业务代码中处理需要阻止事件传播需求的时候就可以避免写出潜在 bug,总结如下:

  • 原生事件中(冒泡阶段 document 事件除外)调用 stopPropagation 或者 stopImmediatePropagation 不仅会阻止后续原生事件触发,还可能造成所有 react 事件不触发
  • 阻止 react 事件传播,在 react 事件监听器中调用 stopPropagation 即可,stopPropagation 对原生事件无影响
  • react 事件中 nativeEvent.stopPropagation 没卵用
  • react 事件中 nativeEvent.stopImmediatePropagation 基本没卵用,仅可阻止原生事件里冒泡阶段 document 事件
返回首页
上一篇react-context-instance下一篇React 原理

Discussion

留言与讨论

想法、补充和不同意见都欢迎。