React中阻止事件冒泡的问题详析( 二 )


除了事件系统,它有自身的一套,另外还需要理解的是,界面上展示的 DOM 与我们代码中的 DOM 组件,也是两样东西,需要在概念上区分开来 。
所以,当你在页面上点击按钮,事件开始在原生 DOM 上走捕获冒泡流程 。React 监听的是 document 上的冒泡阶段 。事件冒泡到 document 后,React 将事件再派发到组件树中,然后事件开始在组件树 DOM 中走捕获冒泡流程 。
现在来尝试理解一下输出结果:
事件最开始从原生 DOM 按钮一路冒泡到 body,body 的事件处理器执行,输出 body 。注意此时流程还没进入 React 。为什么?因为 React 监听的是 document 上的事件 。
继续往上事件冒泡到 document 。
事件到达 document 之后,发现 document 上面一共绑定了三个事件处理器,分别是代码中通过 document.addEventListener 在 ReactDOM.render 前后调用的,以及一个隐藏的事件处理器,是 ReactDOM 绑定的,也就是前面提到的 React 用来代理事件的那个处理器 。
同一元素上如果对同一类型的事件绑定了多个处理器,会按照绑定的顺序来执行 。
所以 ReactDOM.render 之前的那个处理器先执行,输出 document:before react mount 。
然后是 React 的事件处理器 。此时,流程才真正进入 React,走进我们的组件 。组件里面就好理解了,从 button 冒泡到 container,依次输出 。
最后 ReactDOM.render 之后的那个处理器先执行,输出 document:after react mount 。
事件完成了在 document 上的冒泡,往上到了 window,执行相应的处理器并输出 window 。
理解 React 是通过监听 document 冒泡阶段来代理组件中的事件,这点很重要 。同时,区分原生 DOM 与 React 组件,也很重要 。并且,React 组件上的事件处理器接收到的 event 对象也有别于原生的事件对象,不是同一个东西 。但这个对象上有个 nativeEvent 属性,可获取到原生的事件对象,后面会用到和讨论它 。
紧接着的代码的改动中,我们在 body 上阻止了事件冒泡,这样事件在 body 就结束了,没有到达 document,那么 React 的事件就不会被触发,所以 React 组件树中,按钮及容器就没什么反应 。如果没理解到这点,光看表象还以为是 bug 。
进而可以理解,如果在 ReactDOM.render() 之前的的 document 事件处理器上将冒泡结束掉,同样会影响 React 的执行 。只不过这里需要调用的不是 event.stopPropagation(),而是 event.stopImmediatePropagation()。

React中阻止事件冒泡的问题详析

文章插图
输出:
React中阻止事件冒泡的问题详析

文章插图
stopImmediatePropagation 会产生这样的效果,即,如果同一元素上同一类型的事件(这里是 click)绑定了多个事件处理器,本来这些处理器会按绑定的先后来执行,但如果其中一个调用了 stopImmediatePropagation,不但会阻止事件冒泡,还会阻止这个元素后续其他事件处理器的执行 。
所以,虽然都是监听 document 上的点击事件,但 ReactDOM.render() 之前的这个处理器要先于 React,所以 React 对 document 的监听不会触发 。
解答前面按钮未能阻止冒泡的问题
如果你已经忘了,这是相应的代码及输出 。
到这里,已经可以解答为什么 React 组件中 button 的事件处理器中调用 event.stopPropagation() 没有阻止 document 的点击事件执行的问题了 。因为 button 事件处理器的执行前提是事件达到 document 被 React 接收到,然后 React 将事件派发到 button 组件 。既然在按钮的事件处理器执行之前,事件已经达到 document 了,那当然就无法在按钮的事件处理器进行阻止了 。
问题的解决

推荐阅读