问题背景
写react弹窗类组件时,经常遇到这样的需求:点击弹窗外的任意地方,关闭弹窗。我想通过在document上添加事件代理执行关闭逻辑,然后通过阻止打开弹窗的元素上的时间冒泡,这种方式来实现。
import React, { useEffect, useRef } from 'react';
export default function Demo() {
const btnRef = useRef();
useEffect(() => {
document.addEventListener('click', function () {
// 关闭弹窗
close();
})
}, []);
function close(){
// 关闭弹窗逻辑
}
funtion open(e) {
// 打开弹窗逻辑
e.stopPropagation();
}
return (
<div className="App">
<button onClick={onOpen}>打开弹窗</button>
</div>
)
}
但是结果发现 open方法中的stopPropation并不生效,document上的事件仍然正常执行。通过查阅资料,发现react的 SyntheticEvent事件模型是通过在document对象上添加事件代理,来模拟原生dom事件的,所有通过jsx标签上绑定的事件(如本例中的onOpen),都是通过冒泡到document上绑定的代理事件上再进行dispatch处理的。
所以,button上绑定的react事件,是冒泡到document上后才执行的,e.stopPropagation 不可能阻止document上绑定的原生dom事件。
问题解释
可以通过下面的例子来理解:
import React, { useEffect, useRef } from 'react';
export default function Demo() {
const appRef = useRef();
const btnRef = useRef();
useEffect(() => {
document.addEventListener('click', function () {
console.log('document click')
})
btnRef.current.addEventListener('click', function () {
console.log('btn click');
})
appRef.current.addEventListener('click', function () {
console.log('app click');
})
}, []);
function onBtnClick(e) {
console.log('react button click');
}
function onAppClick(e) {
console.log('react app click');
}
return (
<div className="App" ref={appRef} onClick={onAppClick}>
<button ref={btnRef} onClick={onBtnClick}>按钮</button>
</div>
)
}
不添加任何阻止冒泡逻辑时,打印顺序如下
btn click
app click
react button click
react app click
document click
在react 按钮点击事件中阻止冒泡
...
function onBtnClick(e) {
e.stopPropagation();
console.log('react button click');
}
...
打印结果,react app click 没有被打印,app click和 documnet click仍然打印
btn click
app click
react button click
document click
说明 react 事件中添加的阻止冒泡,只能阻止react 类的事件。
在原生按钮点击事件中阻止冒泡
btnRef.current.addEventListener('click', function (e) {
e.stopPropagation();
console.log('btn click');
})
打印结果
btn click
当按钮原生事件上阻止冒泡后,app 和 document上的原生事件无法触发是很容易理解的。然后,由于document上接收不到 按钮原生事件的冒泡,所以react的事件代理机制就失效了,进而导致react绑定的 app click btn click事件都无法触发了。
解决方案
- 通过原生事件阻止冒泡,阻止 document 原生事件;
btnRef.current.addEventListener('click', function (e) {
e.stopPropagation();
console.log('btn click');
})
- 在react事件中调用 e.nativeEvent.stopImmediatePropagation()
stopImmediatePropagation 会阻止元素上所有同类型的事件监听器调用, 具体参考mdn。 这种方式要求用户在document上绑定的事件 要晚于react render
function onBtnClick(e) {
// e.stopPropagation();
e.nativeEvent.stopImmediatePropagation();
console.log('react button click');
}
<button ref={btnRef} onClick={onBtnClick}>按钮</button>
- 在window 上绑定事件
因为window上事件的冒泡顺序位于document之后,所以react 事件中的e.stopPropagation阻止了react在document上的代理事件的冒泡,进而不会触发window上的事件。
总结
react jsx事件回调里的e.stopPropagation 只能阻止react 在document上绑定的代理事件的冒泡行为。