从 Vue 到 React:React 合成事件

目录

    • [一、什么是 React 合成事件?](#一、什么是 React 合成事件?)
    • 二、处理流程
      • [React 事件系统的大致流程](#React 事件系统的大致流程)
      • [和 Vue 3 的区别](#和 Vue 3 的区别)
    • 三、用法示例
    • [四、SyntheticEvent 的特点](#四、SyntheticEvent 的特点)
    • [五、为什么 React 要统一事件到根节点?](#五、为什么 React 要统一事件到根节点?)
      • 1.减少事件监听器数量
      • [2. 简化事件解绑逻辑](#2. 简化事件解绑逻辑)
      • [3. 保证一致的行为](#3. 保证一致的行为)
    • [六、React 18 后事件系统的新变化](#六、React 18 后事件系统的新变化)
      • [1. 合成事件对象变成自动持久化](#1. 合成事件对象变成自动持久化)
      • [2 优先级和过渡(Transition)](#2 优先级和过渡(Transition))
      • [3. 更加灵活的事件监听方式(支持原生事件与合成事件并存)](#3. 更加灵活的事件监听方式(支持原生事件与合成事件并存))
      • [4. 改进事件冒泡机制](#4. 改进事件冒泡机制)

一、什么是 React 合成事件?

官方定义简化版:

合成事件(Synthetic Event) 是 React 自己实现的一套事件系统,模拟了浏览器原生 DOM 事件的所有能力,同时统一了不同浏览器的兼容性差异。

所以,React 不是直接在真实 DOM 上绑定原生事件,而是拦截浏览器事件,自己封装了一套虚拟事件对象,统一管理和派发。

这就是所谓的 Synthetic(合成)


二、处理流程

理解合成事件,需要先知道它内部做了什么事情:

React 事件系统的大致流程

  1. 组件挂载时,不会在每个元素上绑事件;
  2. React 会在根节点 (比如 #root)统一绑定一组原生事件监听器(比如 click、input、change 等);
  3. 所有子组件的事件,都会冒泡到根节点
  4. React 拦截原生事件后,会创建一个SyntheticEvent对象,封装原生事件;
  5. 然后根据事件注册表,依次调用对应组件里绑定的事件处理函数。

和 Vue 3 的区别

特性 Vue 3 React
事件绑定位置 通常直接绑定在元素上(原生事件) 集中到根节点统一监听,冒泡拦截
事件对象 原生 DOM Event SyntheticEvent(封装的虚拟事件对象)
浏览器兼容性处理 框架层面适配部分 完全由 SyntheticEvent 统一处理
事件解绑时机 卸载时手动解绑 React 自动管理合成事件生命周期

三、用法示例

在 React 中绑定事件非常简单,直接用 on事件名 驼峰式命名。

示例:React 里的点击事件

ts 复制代码
function Button() {
  function handleClick(event: React.MouseEvent<HTMLButtonElement>) {
    console.log('按钮被点击了');
    console.log(event.type); // synthetic event
  }

  return <button onClick={handleClick}>点击我</button>;
}
  • onClick(不是 all lowercase 的 onclick
  • 参数 event 是 React 的 SyntheticEvent 对象,不是浏览器原生 Event!

对比 Vue 3 的写法:

html 复制代码
<template>
  <button @click="handleClick">点击我</button>
</template>

<script setup>
function handleClick(event) {
  console.log('按钮被点击了');
  console.log(event.type); // 原生 event
}
</script>
  • Vue 直接就是监听原生事件;
  • 参数 event 是原生的 DOM Event。

四、SyntheticEvent 的特点

特性 说明
自动回收 SyntheticEvent 是池化(pooling)的,事件处理函数执行后对象被回收
统一 API 不同浏览器下,属性和方法一致,无需写浏览器兼容代码
可访问原生事件 event.nativeEvent 可以拿到真正的 DOM Event 对象
支持冒泡和捕获阶段 事件冒泡、捕获机制兼容原生的行为

注意:

  1. React 早期版本(v16及以前)为了性能,在事件处理完后会复用 event 对象 。所以你如果异步访问 event,需要先调用 event.persist()
ts 复制代码
function handleClick(event) {
  event.persist(); // 防止被回收
  setTimeout(() => {
    console.log(event.type); // safe
  }, 1000);
}

不过 React 17+ 以后官方优化了,但这个历史包袱需要了解。

  1. 因为React 的事件委托是通过根节点统一管理的,所以如果你在组件里用window.addEventListener,要记得在组件卸载时解绑,防止内存泄漏。

五、为什么 React 要统一事件到根节点?

我们已经知道了 合成事件 是个什么东西,那么 React 为什么要这么处理呢?

React 采用统一事件委托(Event Delegation)机制的根本目的是:

性能优化 + 统一管理 + 更好的跨浏览器兼容性。

详细拆开来讲:

1.减少事件监听器数量

如果不用事件委托,假设有 100 个按钮,每个按钮都绑定一个 click 事件监听器,浏览器要管理 100 个回调函数,开销非常大,尤其是在频繁增删节点的场景下。

而使用事件委托后:

  • 只需要在 document 或根节点(如 #root)上绑定一次 click 事件;
  • 所有子元素的点击都会冒泡到这个统一的监听器;
  • React 根据事件源(event.target)找到真正的处理逻辑。

这样就减少了 DOM 事件绑定数量,提高了整体性能。

2. 简化事件解绑逻辑

当组件卸载时:

  • 如果每个组件自己绑定事件,需要一一解绑,容易内存泄漏;
  • 而如果统一在根节点处理,只需要管理内部的注册表,不需要频繁触碰原生 DOM API。

可以让事件生命周期管理更简单、可控。

3. 保证一致的行为

不同浏览器对一些事件(如 focus, blur, input, change)的冒泡行为处理不同。

React 自己控制合成事件的冒泡路径,因此可以做到:

  • 行为统一,无论是在 Chrome、Firefox 还是 Safari;
  • API 统一,开发者无需写浏览器兼容判断。

降低了浏览器差异带来的开发和维护成本。

所以React 的事件委托机制主要是为了性能、代码统一、兼容性、易维护四个方面考虑。

六、React 18 后事件系统的新变化

React 18 引入了并发特性(Concurrent Features),带来了一些和事件处理相关的新变化,主要有这些:

1. 合成事件对象变成自动持久化

在 React 17 及之前版本:

  • SyntheticEvent 是有对象池复用的。
  • 每次事件触发后,事件对象会被自动回收(event pooling)
  • 如果你想在异步操作里使用事件对象,需要调用 e.persist() 手动阻止回收。
ts 复制代码
function handleClick(e) {
  e.persist(); // 手动持久化
  setTimeout(() => {
    console.log(e.target); // 否则这里访问不到
  }, 1000);
}

而在 React 18:

  • 彻底取消了 event pooling。
  • 合成事件对象默认就是持久的,不需要再 e.persist()
  • 你可以放心在异步操作、setTimeout、Promise 等里访问事件对象。

好处: 简化开发,避免初学者因忘记 e.persist() 导致的奇怪 bug。

2 优先级和过渡(Transition)

React 18 提供了 startTransition API,专门处理低优先级的更新

示例:

ts 复制代码
import { startTransition } from 'react';

function Search() {
  const [list, setList] = useState([]);

  function handleInput(e) {
    const value = e.target.value;
    startTransition(() => {
      setList(generateBigList(value));
    });
  }

  return <input onChange={handleInput} />;
}
  • 输入时,不会因为大量更新而卡住界面。

3. 更加灵活的事件监听方式(支持原生事件与合成事件并存)

在 React 18+ 中,你可以选择:

  • 用 React 的合成事件系统(默认方式)
  • 或自己直接给 DOM 元素添加原生事件监听器(addEventListener)

比如,某些高频率的原生事件(如 mousemove, scroll):

  • 合成事件虽然有统一机制,但有额外开销;
  • 在极致性能优化下,可以选择直接绑定原生事件。

示例:

ts 复制代码
useEffect(() => {
  const div = document.getElementById('myDiv');
  const handler = () => console.log('mouse move');
  div?.addEventListener('mousemove', handler);

  return () => {
    div?.removeEventListener('mousemove', handler);
  };
}, []);

这种方式,完全绕开了 React 的 Synthetic Event 系统。
所以:

  • 大多数普通事件:继续用 React 合成事件,享受统一管理;
  • 极端高性能场景(高频原生事件):手动绑定原生事件。

4. 改进事件冒泡机制

在并发模式下,React 渲染变得可中断可恢复,所以事件系统也要能适应:

  • 即使组件正在渲染中,事件也不会丢失;
  • 事件冒泡路径的计算更精确,能适配组件的挂起(Suspense)/恢复操作;
  • 在不同优先级的更新之间,合理处理事件(startTransition、defer update 等场景)。

简单来说,**新版事件冒泡机制更智能,更贴合并发渲染特性。

所以我们在实际开发中:

  • 普通项目,放心使用 React 合成事件,不需要刻意切换。
  • 只有在【滚动、鼠标移动、大规模动画监听】等高频场景,再考虑用原生事件。
  • 在异步任务里(比如 setTimeout、Promise)使用事件对象时,不用再 persist,很自然就能访问。
  • 在启用并发特性的项目(如 startTransition),不用担心事件丢失或异常,React 内部已经优化好了。
相关推荐
Hamm3 小时前
用装饰器和ElementPlus,我们在NPM发布了这个好用的表格组件包
前端·vue.js·typescript
明似水3 小时前
Flutter 弹窗队列管理:支持优先级的线程安全通用弹窗队列系统
javascript·安全·flutter
HhhDreamof_4 小时前
云贝餐饮 最新 V3 独立连锁版 全开源 多端源码 VUE 可二开
前端·vue.js·开源
Simaoya4 小时前
【vue】【element-plus】 el-date-picker使用cell-class-name进行标记,type=year不生效解决方法
前端·javascript·vue.js
Dnn014 小时前
vue3+element-push 实现input框粘贴图片或文本,图片上传。
前端·javascript·vue.js
Nuyoah.4 小时前
《Vue3学习手记5》
前端·javascript·学习
曹牧5 小时前
Java 调用webservice接口输出xml自动转义
java·开发语言·javascript
天天扭码5 小时前
2025年了,npm 与 pnpm我们该如何选择
前端·javascript·npm
烛阴5 小时前
10个JavaScript编程技巧,助你成为高效开发高手!
前端·javascript
sen_shan5 小时前
Vue3+Vite+TypeScript+Element Plus开发-23.客制Form组件
vue.js