React的事件处理机制
React 的事件处理机制是为了优化浏览器的原生事件系统,使之更适合在 React 应用中使用。主要特性包括合成事件(SyntheticEvent)和事件池等。
合成事件(SyntheticEvent)
合成事件(SyntheticEvent)是 React 为了解决跨浏览器兼容性和性能问题而引入的一层封装。它提供了一个统一的事件接口,使得开发者可以编写一致的事件处理代码,而不必担心不同浏览器之间的差异。
合成事件的特点
合成事件有三个特点:
- 合成事件确保了不同浏览器之间事件行为的一致性。你不需要为不同的浏览器编写不同的事件处理逻辑。
- React 使用事件委托来提高性能。事件处理器并不是直接绑定到每个 DOM 节点上,而是绑定到根节点上,通过事件冒泡机制来触发具体的处理器。
- 为了减少内存消耗,React 重用事件对象。这意味着在事件处理函数执行完毕后,事件对象会被复用。因此,如果需要在异步回调中使用事件对象的数据,需要先将其属性保存在一个变量中。
第一点我相信大家都能理解,也就是React对各个浏览器进行了兼容,保证了我们用React写出了同样的代码,就有同样的功效,不像某些浏览器写了一样的代码却功能有差别。
至于第二点和第三点,我们要详细解释一下了。
React的事件委托
React 使用事件委托来提高性能。事件处理器并不是直接绑定到每个 DOM 节点上,而是绑定到根节点上,通过事件冒泡机制来触发具体的处理器。
为什么会使用事件委托
- 减少事件处理器的数量:如果一个列表中有大量的项目,直接为每个项目绑定事件处理器会导致大量的事件处理器,这会消耗大量的内存和 CPU 资源。
- 动态添加和删除元素:对于动态生成的内容,直接绑定事件处理器需要在每次内容变化时重新绑定,而事件委托可以避免这种情况。
在 React 中,事件委托是自动实现的。React 不会为每个单独的 DOM 元素绑定事件处理器,而是将事件处理器绑定到文档根节点(通常是 document)。当事件触发时,React 会通过事件冒泡机制捕获事件,并根据事件的目标元素来调用相应的事件处理器。
整一个事件触发的流程是这样的:
flowchart LR
A[原生DOM事件流] -->|捕获阶段| B[浏览器内核]
B -->|冒泡到根节点| C[React根监听器]
C --> D{React事件系统}
D -->|1. 创建合成事件| E[SyntheticEvent]
D -->|2. 重建组件路径| F[虚拟DOM树]
D -->|3. 自定义传播| G[按需执行组件回调]
其中,合成事件可以理解为普通事件的加强版本,拥有一些普通事件没有的功能,之后重建路径,按照这个路径给虚拟dom又来了一遍捕获冒泡,其中在目标阶段时,触发onClick后的回调函数。
事件池(Event Pooling)
React 的 事件池(Event Pooling) 是一种性能优化策略,它通过 复用合成事件(SyntheticEvent)对象 减少内存分配和垃圾回收的负担。
- React 的
SyntheticEvent(合成事件)在触发时会被 临时创建 并放入一个 共享的事件池。 - 当事件回调执行完毕后,React 会 重置事件对象 并放回池中,供后续事件复用。
- 目的:避免频繁创建/销毁事件对象,提高性能(尤其在大量事件触发时)。
事件池中的事件对象大概长这个样子:
| 属性 | 说明 | 示例 |
|---|---|---|
event.target | 触发事件的 DOM 元素(如 <button>)。 | event.target.value(输入框的值) |
event.currentTarget | 当前处理事件的 React 元素(可能不同于 target,由于事件委托)。 | event.currentTarget.getAttribute('id') |
event.type | 事件类型(如 "click"、"change")。 | event.type === "click" |
event.nativeEvent | 原始浏览器事件(MouseEvent、KeyboardEvent)。 | event.nativeEvent.clientX(鼠标位置) |
event.preventDefault() | 阻止默认行为(如表单提交、链接跳转)。 | event.preventDefault() |
event.stopPropagation() | 阻止事件冒泡(仅影响 React 合成事件)。 | event.stopPropagation() |
当有事件触发的时候,就会从池中拿出一个对象,给它赋值,用完了以后就清空里面的值再放池中,准备复用。
所以在老版本中,这么做访问不到对象:
function handleClick(event) {
console.log(event.target); // 正常访问(同步)
setTimeout(() => {
console.log(event.target); // null(事件已被回收)
}, 100);
}
<button onClick={handleClick}>点击</button>
因为在执行完handleClick()后,event对象已经被回收了,里面的值全部清空了。
但是在React17之后的版本,它移除了事件池的功能,合成事件对象 不再被池化,每次事件触发都会创建一个新对象。
虽然它已经被废止了,但是它的设计思想还是很有借鉴意义的,当我们空间不足的时候,可以借鉴这种思想。
总结
我们今天深入探讨了React的事件处理机制,特别是合成事件(SyntheticEvent)和事件委托(Event Delegation)。合成事件确保了跨浏览器的一致性,并通过事件委托提高了性能。事件委托减少了事件处理器的数量,优化了动态内容的处理。虽然事件池在React 17之前通过复用事件对象减少内存消耗,但React 17及之后版本移除了这一功能,每次事件触发都会创建新的事件对象,简化了异步访问并提升了安全性。这些机制共同提升了React应用的性能和用户体验。