跳到主要内容

React 合成事件

问题

什么是 React 合成事件?它和原生事件有什么区别?事件委托是如何实现的?

答案

合成事件(SyntheticEvent) 是 React 对浏览器原生事件的跨浏览器封装。它具有与原生事件相同的接口,但在所有浏览器中行为一致。React 使用事件委托机制,将所有事件统一绑定到根节点。

合成事件的特点

特性合成事件原生事件
绑定位置根节点(root)目标元素
跨浏览器兼容✅ 统一 API❌ 需要兼容处理
事件对象SyntheticEventEvent
事件池化React 16 复用 React 17 移除
阻止冒泡e.stopPropagation()e.stopPropagation()

事件委托机制

React 17 之前

// React 16:事件绑定在 document 上
document.addEventListener('click', reactClickHandler);

// 问题:多个 React 应用可能冲突

React 17+

// React 17+:事件绑定在 React 根容器上
const root = document.getElementById('root');
root.addEventListener('click', reactClickHandler);

合成事件与原生事件的区别

事件类型命名

// React:驼峰命名
<button onClick={handleClick}>Click</button>
<input onChange={handleChange} />
<form onSubmit={handleSubmit}>

// 原生:全小写
button.onclick = handleClick;
input.onchange = handleChange;
form.onsubmit = handleSubmit;

事件处理器

// React:传入函数
<button onClick={handleClick}>Click</button>
<button onClick={() => handleClick(id)}>Click</button>

// 原生:传入函数名字符串(不推荐)
<button onclick="handleClick()">Click</button>

阻止默认行为

// React:必须显式调用 preventDefault
function Form() {
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault(); // 阻止表单提交
// 处理逻辑
};

return <form onSubmit={handleSubmit}>...</form>;
}

// 原生:可以 return false(不推荐)
// <form onsubmit="return false;">

事件对象

function handleClick(e: React.MouseEvent<HTMLButtonElement>) {
// React 合成事件对象
console.log(e.type); // 'click'
console.log(e.target); // 目标元素
console.log(e.currentTarget); // 绑定事件的元素
console.log(e.nativeEvent); // 原生事件对象

// 访问原生事件
console.log(e.nativeEvent.offsetX);
}

SyntheticEvent API

interface SyntheticEvent<T = Element> {
// 通用属性
bubbles: boolean;
cancelable: boolean;
currentTarget: T;
defaultPrevented: boolean;
eventPhase: number;
isTrusted: boolean;
target: EventTarget;
timeStamp: number;
type: string;

// 原生事件
nativeEvent: Event;

// 方法
preventDefault(): void;
stopPropagation(): void;
isPropagationStopped(): boolean;
persist(): void; // React 16 使用,17+ 无需
}

常用事件类型

// 鼠标事件
function handleMouse(e: React.MouseEvent<HTMLDivElement>) {
console.log(e.clientX, e.clientY); // 鼠标位置
console.log(e.button); // 按键
}

// 键盘事件
function handleKey(e: React.KeyboardEvent<HTMLInputElement>) {
console.log(e.key); // 按键值 'Enter', 'a'
console.log(e.keyCode); // 键码(已废弃)
console.log(e.ctrlKey); // 是否按下 Ctrl
}

// 表单事件
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
console.log(e.target.value);
}

// 焦点事件
function handleFocus(e: React.FocusEvent<HTMLInputElement>) {
console.log(e.relatedTarget); // 之前/之后聚焦的元素
}

// 剪贴板事件
function handlePaste(e: React.ClipboardEvent<HTMLInputElement>) {
console.log(e.clipboardData.getData('text'));
}

事件冒泡与捕获

冒泡阶段(默认)

function App() {
return (
<div onClick={() => console.log('div clicked')}>
<button onClick={() => console.log('button clicked')}>
Click me
</button>
</div>
);
}

// 点击按钮输出:
// button clicked
// div clicked

捕获阶段

function App() {
return (
<div onClickCapture={() => console.log('div capture')}>
<button
onClickCapture={() => console.log('button capture')}
onClick={() => console.log('button bubble')}
>
Click me
</button>
</div>
);
}

// 点击按钮输出:
// div capture(捕获阶段)
// button capture(捕获阶段)
// button bubble(冒泡阶段)
// div bubble(冒泡阶段,如果有 onClick)

阻止冒泡

function Child() {
const handleClick = (e: React.MouseEvent) => {
e.stopPropagation(); // 阻止事件冒泡
console.log('Child clicked');
};

return <button onClick={handleClick}>Click</button>;
}

function Parent() {
return (
<div onClick={() => console.log('Parent clicked')}>
<Child />
</div>
);
}

// 点击按钮只输出:Child clicked

合成事件与原生事件混用

执行顺序

function App() {
const buttonRef = useRef<HTMLButtonElement>(null);

useEffect(() => {
const button = buttonRef.current!;

// 原生事件 - 直接绑定
button.addEventListener('click', () => {
console.log('1. Native button');
});

// 原生事件 - document
document.addEventListener('click', () => {
console.log('4. Native document');
});
}, []);

return (
<div onClick={() => console.log('3. React div')}>
<button
ref={buttonRef}
onClick={() => console.log('2. React button')}
>
Click
</button>
</div>
);
}

// 点击按钮输出顺序:
// 1. Native button(原生事件捕获/目标阶段)
// 2. React button(合成事件,绑定在 root)
// 3. React div(合成事件冒泡)
// 4. Native document(原生事件冒泡到 document)

阻止原生事件冒泡

function App() {
useEffect(() => {
document.addEventListener('click', () => {
console.log('Document clicked');
});
}, []);

const handleClick = (e: React.MouseEvent) => {
// stopPropagation 只阻止 React 合成事件冒泡
e.stopPropagation();

// 要阻止原生事件,需要访问 nativeEvent
e.nativeEvent.stopImmediatePropagation();

console.log('Button clicked');
};

return <button onClick={handleClick}>Click</button>;
}
注意

e.stopPropagation() 只阻止 React 合成事件的冒泡,不影响原生事件。要同时阻止原生事件,需要使用 e.nativeEvent.stopImmediatePropagation()

事件池化(已移除)

React 16 的事件池化

// React 16:事件对象会被复用
function handleClick(e: React.MouseEvent) {
console.log(e.type); // 'click'

setTimeout(() => {
console.log(e.type); // null(事件对象已被重置)
}, 0);
}

// 需要调用 e.persist() 保留事件对象
function handleClick(e: React.MouseEvent) {
e.persist();
setTimeout(() => {
console.log(e.type); // 'click'
}, 0);
}

React 17+ 移除事件池化

// React 17+:事件对象不再复用
function handleClick(e: React.MouseEvent) {
setTimeout(() => {
console.log(e.type); // 'click' ✅
}, 0);
}

自定义事件

使用 CustomEvent

// 触发自定义事件
function Publisher() {
const triggerEvent = () => {
const event = new CustomEvent('myCustomEvent', {
detail: { message: 'Hello from publisher' },
bubbles: true,
});
document.dispatchEvent(event);
};

return <button onClick={triggerEvent}>Publish</button>;
}

// 监听自定义事件
function Subscriber() {
const [message, setMessage] = useState('');

useEffect(() => {
const handler = (e: CustomEvent) => {
setMessage(e.detail.message);
};

document.addEventListener('myCustomEvent', handler as EventListener);
return () => {
document.removeEventListener('myCustomEvent', handler as EventListener);
};
}, []);

return <div>Message: {message}</div>;
}

使用 EventEmitter

import { EventEmitter } from 'events';

const eventBus = new EventEmitter();

function Publisher() {
const handleClick = () => {
eventBus.emit('notification', { type: 'success', message: 'Done!' });
};

return <button onClick={handleClick}>Notify</button>;
}

function Subscriber() {
const [notification, setNotification] = useState<Notification | null>(null);

useEffect(() => {
const handler = (data: Notification) => setNotification(data);
eventBus.on('notification', handler);
return () => { eventBus.off('notification', handler); };
}, []);

return notification && <div>{notification.message}</div>;
}

常见面试问题

Q1: React 事件和原生事件有什么区别?

答案

区别React 合成事件原生事件
绑定位置根容器(React 17+)目标元素
命名驼峰(onClick)小写(onclick)
处理器函数函数或字符串
事件对象SyntheticEventEvent
跨浏览器✅ 兼容❌ 需处理
默认行为必须 preventDefault()可 return false
// React
<button onClick={(e) => { e.preventDefault(); }}>

// 原生
button.addEventListener('click', (e) => { e.preventDefault(); });

Q2: React 17 事件系统有什么变化?

答案

  1. 事件绑定位置改变:从 document 改为 React 根容器
// React 16
document.addEventListener('click', handler);

// React 17
rootElement.addEventListener('click', handler);
  1. 好处

    • 多个 React 版本可以共存
    • 与其他框架更好地集成
    • 微前端场景下事件不冲突
  2. 移除事件池化:不再需要 e.persist()

Q3: 为什么 React 使用合成事件?

答案

  1. 跨浏览器兼容:统一 API,屏蔽浏览器差异
// 不同浏览器的差异被 React 处理
e.target // 统一获取目标元素
e.key // 统一获取按键
  1. 性能优化:事件委托减少内存占用
// 1000 个按钮只绑定一个事件
<div>
{items.map(item => (
<button onClick={handleClick}>{item}</button>
))}
</div>
  1. 与 React 架构集成:批量更新、优先级调度

Q4: React 中如何阻止事件冒泡到原生事件?

答案

function Button() {
const handleClick = (e: React.MouseEvent) => {
// 阻止 React 合成事件冒泡
e.stopPropagation();

// 阻止原生事件继续传播
e.nativeEvent.stopImmediatePropagation();
};

return <button onClick={handleClick}>Click</button>;
}
方法作用
e.stopPropagation()阻止 React 合成事件冒泡
e.nativeEvent.stopPropagation()阻止原生事件冒泡
e.nativeEvent.stopImmediatePropagation()阻止原生事件冒泡,且阻止同元素其他监听器

Q5: React 事件和原生事件的执行顺序是什么?

答案

// 执行顺序
// 1. 原生事件捕获阶段(document → 目标)
// 2. 原生事件目标阶段(目标元素直接绑定)
// 3. 原生事件冒泡阶段(目标 → root)
// 4. React 合成事件(在 root 处理)
// 5. 原生事件继续冒泡(root → document)

记忆口诀:原生先执行,合成在 root,document 最后。

相关链接