React中的事件机制
什么是合成事件?
在React中,合成事件(Synthetic Events)
是一种跨浏览器的事件包装机制,旨在统一浏览器的事件处理方式,解决跨浏览器兼容性问题,并提供更高效、更一致的事件处理体验。React 的合成事件系统是在 React 的事件系统基础上构建的,它为 DOM 事件提供了一个标准化的接口,使得事件处理程序不需要考虑不同浏览器之间的差异。简单来说,它是对浏览器原生事件的一个封装。
合成事件对象是 SyntheticEvent 的实例,具有与原生事件相同的方法和属性,如 preventDefault(), stopPropagation() 等。如果想要获得原⽣DOM事件对象,可以通过 e.nativeEvent
属性获取。
// 原生事件回调处理,分别在回调函数中打印事件对象,原生事件和合成事件对象如下图所示
<button onclick="handleClick()">原生事件</button>
// react中合成事件回调处理
<button onClick={handleClick}>react合成事件</button>
使用合成事件的好处
- 跨浏览器一致性: 原生 DOM 事件在不同的浏览器中有些行为差异(如事件的触发方式、事件对象的属性等)。React 的合成事件系统统一了这些差异,使得开发者可以编写兼容所有浏览器的事件处理代码。
- 性能优化: React 通过事件委托的方式,将事件处理绑定到root元素上,事件冒泡时,React 会通过事件池重用合成事件对象,从而避免频繁创建和销毁事件对象,减少性能开销。
- 事件池(Event Pooling): React 使用事件池技术来重用事件对象。在事件处理程序被调用后,合成事件对象会被放回事件池,防止频繁创建和销毁事件对象,从而提高性能。
- 简化的 API: React 的合成事件为开发者提供了一致、简单的 API,使得事件的处理更加直观。
事件委托
在 React 组件中,通常会内联编写事件处理。但是,对大多数事件来说,React 实际上并不会将它们附加到 DOM 节点上。相反,React会直接在 document 节点上(在 React 17 中,React 将不再向 document 附加事件处理器。而会将事件处理器附加到渲染 React 树的根 DOM 容器中)为每种事件类型附加一个处理器。这被称为事件委托。
当 document 上触发 DOM 事件时,React 会找出调用的组件,然后 React 事件会在组件中向上 “冒泡”。但实际上,原生事件已经冒泡出了 document 级别,React 在其中安装了事件处理器。
import React, { useEffect} from 'react'
export default function App() {
useEffect(() => {
const parent = document.getElementById('react')
parent.addEventListener('click', e => console.log('addEventListener click'))
}, [])
return (
<div id="react" style={{width: '100px', height: '100px', backgroundColor: 'red'}}
onClick={(e) => console.log('react onClick')}>
</div>
)
}
如下图,通过Event Listeners
选项卡不难发现,React内部将onClick事件确实代理到了root上,而原生addEventListener添加的事件则是绑定在child元素上。
虽然 onClick 看似绑定到 DOM 元素上,但实际上并不会把事件代理函数直接绑定到真实的节点上,而是把所有的事件绑定到root节点上,使用⼀个统⼀的事件去监听。这个事件监听器上维持了⼀个映射来保存所有组件内部的事件监听和处理函数。当组件挂载或卸载时,只是在这个统⼀的事件监听器上插⼊或删除⼀些对象。当事件发⽣时,首先被这个统⼀的事件监听器处理,然后在映射⾥找到真正的事件处理函数并调⽤。这样做简化了事件处理和回收机制,效率也有很大提升。
事件池
SyntheticEvent
对象会被放入池中统一管理。React 为了提高性能,会重用事件对象。当事件处理函数执行完后,合成事件会被放入事件池中,并且会在事件处理完成后将这些对象清空。因此,如果需要异步访问事件对象的属性或方法,需要调用 event.persist()
来避免被回收。
function handleClick(event) {
event.persist(); // 阻止事件池回收
setTimeout(() => {
console.log(event.target); // 异步访问事件对象时,需要调用 persist
}, 1000);
}
从 v17 开始,e.persist() 将不再生效,因为 SyntheticEvent 不再放入事件池中。
React事件执行顺序
import React, { useEffect, useRef } from 'react'
import './index.css'
export default function App() {
const parentRef = useRef(null)
const childRef = useRef(null)
useEffect(() => {
const root = document.querySelector('#root')
root.addEventListener('click', e => {
console.log('原生事件: root DOM监听')
})
parentRef.current.addEventListener('click', e => {
console.log('原生事件: parent DOM监听')
})
childRef.current.addEventListener('click', e => {
console.log('原生事件: child DOM监听')
// e.stopPropagation() // 阻止原生事件冒泡
})
}, [])
return (
<div>
<div id="parent" ref={parentRef} onClick={(e) => {
console.log('React事件:parent click')
// e.nativeEvent.stopImmediatePropagation() // 合成事件冒泡完成后阻止执行root节点的原生事件
}}>
<div id="child" ref={childRef} onClick={(e) => {
console.log('React事件:child click')
// e.stopPropagation() // 阻止合成事件冒泡
}}></div>
</div>
</div>
)
}
可以发现,当点击子元素时,事件执行顺序如下图:
点击父元素时,事件执行顺序如下图:
因此可以得出结论,React中的合成事件和原生事件执行顺序是:
1、当真实 DOM 元素触发事件时会先执行该元素上的原生事件,并冒泡以及执行冒泡到的元素上的原生事件;
2、当冒泡到root节点时,会进行一个派发操作dispatchEvent
以执行触发事件的目标元素上的React合成事件,并逐渐冒泡并执行这些元素上的React合成事件(如果有的话);
3、当冒泡到root节点时,最后执行root节点上的原生事件。
参考:React合成事件——老版文档.