到这里,已经可以解答为什么 React 组件中 button 的事件处理器中调用 event.stopPropagation() 没有阻止 document 的点击事件执行的问题了。因为 button 事件处理器的执行前提是事件达到 document 被 React 接收到,然后 React 将事件派发到 button 组件。既然在按钮的事件处理器执行之前,事件已经达到 document 了,那当然就无法在按钮的事件处理器进行阻止了。
问题的解决
要解决这个问题,这里有不止一种方法。
用 window 替换 document
来自 中提供的这个方法是最快速有效的。使用 window 替换掉 document 后,前面的代码可按期望的方式执行。
function App() { useEffect(() => { + window.addEventListener("click", documentClickHandler); return () => { + window.removeEventListener("click", documentClickHandler); }; }, []); function documentClickHandler() { console.log("document clicked"); } function btnClickHandler(event) { event.stopPropagation(); console.log("btn clicked"); } return <button onClick={btnClickHandler}>CLICK ME</button>; }
这里 button 事件处理器上接到到的 event 来自 React 系统,也就是 document 上代理过来的,所以通过它阻止冒泡后,事件到 document 就结束了,而不会往上到 window。
Event.stopImmediatePropagation()
组件中事件处理器接收到的 event 事件对象是 React 包装后的 SyntheticEvent 事件对象。但可通过它的 nativeEvent 属性获取到原生的 DOM 事件对象。通过调用这个原生的事件对象上的 stopImmediatePropagation() 方法可达到阻止冒泡的目的。
function btnClickHandler(event) { + event.nativeEvent.stopImmediatePropagation(); console.log("btn clicked"); }
至于原理,其实前面已经有展示过。React 在 render 时监听了 document 冒泡阶段的事件,当我们的 App 组件执行时,准确地说是渲染完成后(useEffect 渲染完成后执行),又在 document 上注册了 click 的监听。此时 document 上有两个事件处理器了,并且组件中的这个顺序在 React 后面。
当调用 event.nativeEvent.stopImmediatePropagation() 后,阻止了 document 上同类型后续事件处理器的执行,达到了想要的效果。
但这种方式有个缺点很明显,那就是要求需要被阻止的事件是在 React render 之后绑定,如果在之前绑定,是达不到效果的。
通过元素自身来绑定事件处理器
当绕开 React 直接通过调用元素自己身上的方法来绑定事件时,此时走的是原生 DOM 的流程,都没在 React 的流程里面。
function App() { const btnElement = useRef(null); useEffect(() => { document.addEventListener("click", documentClickHandler); if (btnElement.current) { btnElement.current.addEventListener("click", btnClickHandler); } return () => { document.removeEventListener("click", documentClickHandler); if (btnElement.current) { btnElement.current.removeEventListener("click", btnClickHandler); } }; }, []); function documentClickHandler() { console.log("document clicked"); } function btnClickHandler(event) { event.stopPropagation(); console.log("btn clicked"); } return <button ref={btnElement}>CLICK ME</button>; }
很明显这样是能解决问题,但你根本不会想要这样做。代码丑陋,不直观也不易理解。
结论
注意区分 React 组件的事件及原生 DOM 事件,一般情况下,尽量使用 React 的事件而不要混用。如果必需要混用比如监听 document,window 上的事件,处理 mousemove,resize 等这些场景,那么就需要注意本文提到的顺序问题,不然容易出 bug。