
一句话:React 自己实现了一套合成事件(SyntheticEvent)系统,事件不绑在元素上而是委托到根容器统一处理;这带来了跨浏览器一致性与性能优势,也带来几个「与原生不一致」的细节(最典型的是
onChange的行为)。
一、合成事件是什么
function Button() {
const handleClick = (e: React.MouseEvent) => {
console.log(e.type); // 'click'
console.log(e.target); // 真实触发的 DOM 元素
console.log(e.currentTarget); // 绑定监听器的元素(这里就是 button)
console.log(e.nativeEvent); // ⭐ 原生事件对象
};
return ;
}
合成事件对象的本质:
它是对【原生事件对象】的一层跨浏览器包装(不是简单的 Proxy),
提供了统一的属性与方法(preventDefault、stopPropagation、target、currentTarget…)
并做掉了历史浏览器之间的差异(如 IE 的 attachEvent、event.target 兼容)
原生事件对象可以通过 e.nativeEvent 拿到。
二、为什么要用合成事件
| 原因 | 说明 |
|---|---|
| 跨浏览器一致性 | 统一属性名与行为,屏蔽历史浏览器差异(如 stopPropagation 的实现差异) |
| 事件委托 | 所有事件都挂在根容器上统一分发,减少监听器数量(性能 + 内存) |
| 动态元素自动生效 | 列表项增删无需重新绑定(因为监听在根上) |
| 与 React 的更新机制协同 | 事件处理里的 setState 能被正确批处理(React 知道「这次更新发生在事件里」) |
| 明确的事件类型体系 | TypeScript 里提供 React.MouseEvent / ChangeEvent 等精确类型 |
// 1000 行的列表,每个都有 onClick:
// ❌ 原生方式:1000 个监听器
{list.map((item) => (
handle(item)}>
))}
// ✅ React 方式:内部只挂一个(在根容器上),点击时按 event.target 找到对应组件的处理函数
// → 对开发者来说写法相同,但 React 内部做了优化
三、事件委托到根容器(React 17 的变化)
React 16 及之前:所有事件监听挂在 document 上
React 17+ :挂在【根容器】(ReactDOM.createRoot 的容器元素)上
ReactDOM.createRoot(document.getElementById('root')).render( );
↑ 事件委托到这里
为什么 React 17 要改:
原因
说明
多版本 React 共存
微前端/多根渲染时,都挂在 document 上会互相干扰;挂在各自根上就互不影响
渐进式升级
页面不同区域可以用不同版本的 React(升级期间)
更贴近原生语义
「事件不应冒泡出根容器」变成现实(因为监听器就在根上)
// 这个变化带来的一个可观察差异:
// 如果子应用(React 16)和外层应用(React 17+)混用,
// 外层【原生】监听在 document 上的事件会先于子应用的 React 事件触发。
四、事件的绑定方式
// ① 内联箭头函数(最常用)
// 优点:能传参、写法简洁
// 缺点:每次渲染创建新函数(若子组件是 memo 的,会导致它重渲染)
// ② 传函数引用(推荐用于纯展示子组件)
// ③ 绑定参数:用 data-* 属性代替闭包
;
function handleClick(e: React.MouseEvent) {
const id = e.currentTarget.dataset.id;
}
// 优点:函数引用稳定(不随渲染变化)
// ④ useCallback 包裹(需要稳定引用时)
const handleClick = useCallback((id: string) => deleteItem(id), [deleteItem]);
;
// ⑤ 类组件的三种绑定方式
class A extends React.Component {
// 方式一:构造函数里 bind
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
handleClick() { console.log(this); }
// 方式二:类字段箭头函数(推荐)
handleClick2 = () => { console.log(this); };
// 方式三:渲染时 bind(⚠️ 每次渲染都创建新函数)
render() {
return (
<>
>
);
}
}
函数组件没有 this 问题——这也是函数组件更简单的原因之一。
五、事件对象:target 与 currentTarget
function List() {
const handleClick = (e: React.MouseEvent) => {
console.log(e.target); // ⭐ 实际被点击的最深元素(可能是 )
console.log(e.currentTarget); // ⭐ 绑定监听器的元素()
};
return (
- 项目一
);
}
关键差异:
target = 事件最初发生的元素(会随着冒泡保持不变)
currentTarget = 当前正在处理该事件的元素(即绑定 onClick 的那个)
⚠️ 陷阱:在异步回调里 currentTarget 会变成 null
setTimeout(() => console.log(e.currentTarget), 0); // null
✅ 需要保存就先取出来:const el = e.currentTarget;
// 事件委托的典型写法(利用 target + closest)
{
const li = (e.target as HTMLElement).closest('li');
if (li) handleSelect(li.dataset.id!);
}}>
{list.map((it) => (
-
{it.name}
))}
六、onChange 的特殊行为(高频考点)
⚠️ React 的 onChange 与原生 change **不是同一个事件**:
原生 DOM:
input 事件 → 每次输入字符都触发(实时)
change 事件 → 失焦后才触发(对文本框而言)
React:
onChange → 等价于原生的 input 事件(实时触发)
→ 所以 React 里写 onChange 就能拿到「每次按键后的值」
// React
console.log(e.target.value)} /> // 每敲一个字符都触发
// 等价的原生写法
element.addEventListener('input', (e) => console.log(e.target.value));
场景
React 写法
等价原生事件
文本框实时输入
onChange
input
提交时/失焦时
onBlur
blur
表单提交
onSubmit
submit
选择变化(select)
onChange
change(select 的 input 与 change 都会触发)
// 如果要「失焦才更新」,用 onBlur
setValue(e.target.value)}
/>
七、事件池(React 16 有,17 移除)
// React 16:事件对象会被「池化」复用,异步访问会拿到 null
function Comp() {
const handleClick = (e) => {
setTimeout(() => {
console.log(e.type); // ❌ React 16 里是 null
}, 0);
};
return ;
}
// React 16 的解法:e.persist() 阻止回收
// React 17+:事件池已被移除,异步访问完全正常,e.persist() 是空操作(保留 API 兼容)
现在不需要关心事件池了,但面试可能问到「React 16 里为什么异步拿不到 event 对象」——答案是事件对象被池化复用。
八、常见事件速查
// 鼠标
onClick / onDoubleClick / onContextMenu
onMouseDown / onMouseUp / onMouseMove / onMouseEnter / onMouseLeave
// 注意 React 的 onMouseEnter / onMouseLeave 不冒泡(与原生一致语义)
// 键盘
onKeyDown / onKeyUp / onKeyPress(已废弃)
// 判断某个键:e.key === 'Enter' / e.code === 'Enter'
// ⚠️ 中文输入时判断 e.nativeEvent.isComposing
// 表单
onChange / onInput / onSubmit / onReset / onFocus / onBlur
onInvalid / onSelect / onScroll(表单元素也可滚)
// 触摸与指针
onTouchStart / onTouchMove / onTouchEnd
onPointerDown / onPointerMove / onPointerUp
onPointerCancel / onGotPointerCapture(指针捕获)
// 拖拽
onDragStart / onDragOver / onDrop / onDragEnd
// 焦点
onFocus / onBlur(React 里这两个会冒泡,是合成事件的差异之一 ⚠️)
// 图片/媒体
onLoad / onError / onPlay / onPause / onTimeUpdate / onEnded
// 动画与过渡
onAnimationStart / onAnimationEnd / onTransitionEnd
// 滚动
onScroll(⚠️ 在 React 中不冒泡,需要绑在具体滚动容器上)
// 一个实用细节:需要 passive 监听器时用原生事件
// React 没有直接暴露 passive 选项(React 17+ 对 touch/wheel 已默认 passive)
// 需要精细控制时用 ref + addEventListener
useEffect(() => {
const el = ref.current;
if (!el) return;
const onWheel = (e: WheelEvent) => { /* ... */ };
el.addEventListener('wheel', onWheel, { passive: false });
return () => el.removeEventListener('wheel', onWheel);
}, []);
九、常见坑
// ① onChange 误以为是原生 change(失焦才触发)
// React 里其实是实时触发(等价 input)
// ② 写成小写的事件名
// ⚠️ React 不识别,会在控制台警告
// ③ 直接调用函数