React 合成事件机制
React 事件委托和事件池的工作原理
问题
解释 React 合成事件机制,包括事件委托和事件池。
解答
什么是合成事件
React 实现了一套自己的事件系统,将浏览器原生事件封装成 SyntheticEvent 对象,提供跨浏览器一致的 API。
function Button() {
const handleClick = (e) => {
// e 是 SyntheticEvent,不是原生事件
console.log(e.constructor.name); // SyntheticBaseEvent
console.log(e.nativeEvent); // 原生事件对象
};
return <button onClick={handleClick}>点击</button>;
}
事件委托
React 不会把事件绑定到具体 DOM 节点,而是统一绑定到根容器,通过事件冒泡统一处理。
// React 17+ 绑定到 root container
// React 16 及之前绑定到 document
function App() {
return (
<div onClick={() => console.log('div')}>
<button onClick={() => console.log('button')}>
点击
</button>
</div>
);
}
// 点击 button 输出:
// button
// div
事件委托的执行流程:
function Demo() {
const rootRef = React.useRef(null);
React.useEffect(() => {
// 原生事件 - 捕获阶段
rootRef.current.addEventListener('click', () => {
console.log('原生捕获');
}, true);
// 原生事件 - 冒泡阶段
rootRef.current.addEventListener('click', () => {
console.log('原生冒泡');
});
}, []);
return (
<div ref={rootRef}>
<button
onClickCapture={() => console.log('React 捕获')}
onClick={() => console.log('React 冒泡')}
>
点击
</button>
</div>
);
}
// 点击输出顺序:
// 原生捕获
// React 捕获
// React 冒泡
// 原生冒泡
事件池(React 16 及之前)
React 16 使用事件池复用事件对象,事件回调执行后属性会被清空:
// React 16 的行为
function OldBehavior() {
const handleClick = (e) => {
console.log(e.type); // 'click'
setTimeout(() => {
console.log(e.type); // null(已被回收)
}, 0);
// 需要调用 persist() 保留事件
// e.persist();
};
return <button onClick={handleClick}>点击</button>;
}
React 17+ 移除了事件池:
// React 17+ 的行为
function NewBehavior() {
const handleClick = (e) => {
console.log(e.type); // 'click'
setTimeout(() => {
console.log(e.type); // 'click'(正常访问)
}, 0);
};
return <button onClick={handleClick}>点击</button>;
}
阻止事件传播
function StopPropagation() {
const handleParent = () => console.log('parent');
const handleChild = (e) => {
e.stopPropagation(); // 阻止合成事件冒泡
console.log('child');
};
return (
<div onClick={handleParent}>
<button onClick={handleChild}>点击</button>
</div>
);
}
// 只输出:child
合成事件与原生事件混用
function MixedEvents() {
const buttonRef = React.useRef(null);
React.useEffect(() => {
// 原生事件绑定在目标元素
buttonRef.current.addEventListener('click', (e) => {
e.stopPropagation(); // 阻止原生事件冒泡
console.log('原生事件');
});
document.addEventListener('click', () => {
console.log('document');
});
}, []);
return (
<button
ref={buttonRef}
onClick={() => console.log('React 事件')}
>
点击
</button>
);
}
// React 17+:原生事件 -> React 事件
// React 16:原生事件(document 上的 React 事件被阻止)
关键点
- 事件委托:React 17+ 将事件绑定到 root container,React 16 绑定到 document
- 合成事件:跨浏览器封装,通过
e.nativeEvent访问原生事件 - 事件池已移除:React 17 移除事件池,不再需要
e.persist() - 执行顺序:原生捕获 → React 捕获 → React 冒泡 → 原生冒泡
- 混用注意:原生事件的
stopPropagation会影响 React 事件(React 16 影响更大)
目录