React 的合成事件是什么?和原生事件有什么区别?
React 给事件处理函数传的是包装过的事件对象,接口和原生事件一致,原生事件在 e.nativeEvent 上。React 17 起事件监听挂在根容器上而不是 document,并去掉了事件池。onScroll 不冒泡,onChange 每次输入都触发,和原生 change 不同。
简答
在 React 里写
<button onClick={(e) => ...}>,e不是浏览器原生的事件对象,而是 React 包装过的一个对象,文档叫它 React 事件对象,也常被叫作合成事件。它的属性和方法跟原生事件基本一样,比如target、preventDefault()、stopPropagation(),同时抹平了一些浏览器差异。需要原生事件时,从e.nativeEvent读。React 并不是给每个按钮都调用
addEventListener。它在根节点上统一监听各类事件,事件冒泡到根节点后,React 再根据触发事件的元素找到对应的组件,按组件树的顺序调用你写的处理函数,这就是事件委托。React 17 之前监听挂在document上,17 起改成挂在createRoot传入的根容器上,这样同一个页面里嵌入多个 React 版本也不会互相干扰。和原生事件的几个区别要记住。React 17 去掉了事件池,旧版本里事件对象在处理函数执行完后会被清空复用,异步回调里读
e.target会出问题,现在没有这个限制了。除了onScroll,React 里的事件都会沿组件树冒泡。onChange每输入一个字符都会触发,行为对应原生的input事件,而不是原生change那样失焦才触发。要注意,React 的冒泡是沿着组件树走的,所以通过 Portal 渲染到
body下的弹窗,点击事件仍然会冒泡到 React 父组件上,哪怕它们在 DOM 里并不是父子关系。
React 事件和原生事件谁先执行
下面在同一个按钮上同时有 React 监听和原生监听,外层 document 也加了一个原生监听:
function App() {
const ref = useRef(null);
useEffect(() => {
const button = ref.current;
const onNative = () => console.log('按钮原生');
const onDocument = () => console.log('document 原生');
button.addEventListener('click', onNative);
document.addEventListener('click', onDocument);
return () => {
button.removeEventListener('click', onNative);
document.removeEventListener('click', onDocument);
};
}, []);
return <button ref={ref} onClick={() => console.log('React')}>点我</button>;
}
点击按钮,依次输出“按钮原生”“React”“document 原生”。
为什么按钮自己的原生监听先于 React?因为 React 的处理函数并没有绑在按钮上,而是等事件冒泡到根容器时才统一分发。事件先在按钮上触发原生监听,再往上冒泡到根容器,React 才调用 onClick,然后事件继续冒泡到 document。
这也说明,在 React 的 onClick 里调用 e.stopPropagation(),能阻止事件继续冒泡到 document 上的原生监听,却挡不住按钮自己的原生监听,因为那个已经执行过了。在 React 17 之前,React 监听挂在 document 上,这种情况下阻止冒泡就拦不住 document 上别的监听,这正是 17 改成挂在根容器上的原因之一。
事件池去掉以后有什么变化
React 16 及更早版本,为了在老浏览器上节省内存,会复用事件对象,处理函数一结束就把所有字段设成 null:
function handleChange(e) {
setData((data) => ({ ...data, text: e.target.value }));
}
这段代码在 React 16 里会报错,因为 setData 的更新函数执行时,e 已经被清空了,当时要先调用 e.persist() 才行。React 17 移除了事件池,现在这样写没有问题。
常见追问
React 里怎么阻止默认行为?能不能 return false?
要显式调用
e.preventDefault()。原生 HTML 里内联处理函数return false可以阻止默认行为,React 里不行。
捕获阶段的事件怎么写?
在事件名后面加
Capture,比如onClickCapture。它会在捕获阶段执行,比子元素的onClick更早,适合做埋点这类不管子元素是否阻止冒泡都要执行的逻辑。
为什么 onFocus 和 onBlur 能冒泡?
原生的
focus和blur不冒泡。React 17 起onFocus、onBlur底层改用原生的focusin、focusout,它们会冒泡,所以父元素也能监听到子元素获得或失去焦点。