React 的合成事件(Synthetic Event)和原生事件(Native Event)的执行顺序
#react
目录
- 1. 总结
- 2. 事件执行顺序
- 3. 更复杂的嵌套结构来展示事件传播
- 4. 主要区别
- 5. 阻止事件传播
- 6. 实际应用场景
- 7. 注意事项
- 8. React 17 前后的事件委托变化
- 9. 性能考虑
- 10. React 的事件系统实现原理
1. 总结
- 执行顺序:==原生捕获 → React 捕获 → React 冒泡 → 原生冒泡==
- document 原生事件:捕获阶段
- 子元素 原生事件:捕获阶段
- 父元素 React事件:捕获
- 子元素 React事件:捕获
- 子元素 React事件:冒泡
- 父元素 React事件:冒泡
- 子元素 原生事件:冒泡阶段
- document 原生事件:冒泡阶段
- 事件委托
- React 事件是委托到
root节点
统一管理- 使得==多个 React 版本共存==成为可能
- React 事件是委托到
- 阻止事件传播
- e.stopPropagation();
- 阻止==React事件==传播
- e.nativeEvent.stopImmediatePropagation();
- 阻止==原生事件==传播
- e.stopPropagation();
- ==合成事件==的特点:
- ==抹平==了浏览器之间的兼容性差异
- 事件代理
- 对事件进行==统一的处理和优化==
- 符合 W3C 规范
- 可以通过
e.nativeEvent
访问原生事件对象
- 事件系统实现原理
- 事件注册阶段
- React 在初始化时,会在 root 容器上注册所有需要的事件(如 click、change 等)
- 所有事件都会被存储在一个 Map 中进行统一管理
- 通过 JSX 中声明的事件,会存储在组件的 fiber 节点中
- 事件触发阶段
- 事件触发时,首先生成合成事件对象
- 按照事件传播的特性(捕获 -> 目标 -> 冒泡)遍历节点
- 调用对应的事件处理函数
- 事件注册阶段
- 事件池
另外可参考 21. React 事件系统
2. 事件执行顺序
function App() {
const divRef = useRef(null);
useEffect(() => {
const div = divRef.current;
// 添加原生事件监听器
div.addEventListener('click', () => {
console.log('原生事件:冒泡阶段');
});
div.addEventListener('click', () => {
console.log('原生事件:捕获阶段');
}, true); // true表示捕获阶段
return () => {
// 清理事件监听
div.removeEventListener('click');
};
}, []);
return (
<div
ref={divRef}
onClick={() => console.log('React事件:冒泡阶段')}
onClickCapture={() => console.log('React事件:捕获阶段')}>
点击我
</div>
);
}
当点击div时,事件执行顺序为:
- 原生事件(捕获阶段)
- React事件(捕获阶段)
- React事件(冒泡阶段)
- 原生事件(冒泡阶段)
3. 更复杂的嵌套结构来展示事件传播
function Parent() {
useEffect(() => {
document.addEventListener('click', () => {
console.log('document 原生事件:捕获阶段');
}, true);
document.addEventListener('click', () => {
console.log('document 原生事件:冒泡阶段');
});
}, []);
return (
<div
onClick={() => console.log('父元素 React事件:冒泡')}
onClickCapture={() => console.log('父元素 React事件:捕获')}
>
<Child />
</div>
);
}
function Child() {
const childRef = useRef(null);
useEffect(() => {
const child = childRef.current;
child.addEventListener('click', () => {
console.log('子元素 原生事件:冒泡阶段');
});
child.addEventListener('click', () => {
console.log('子元素 原生事件:捕获阶段');
}, true);
}, []);
return (
<button
ref={childRef}
onClick={() => console.log('子元素 React事件:冒泡')}
onClickCapture={() => console.log('子元素 React事件:捕获')}
>
点击我
</button>
);
}
点击按钮时的执行顺序:
1. document 原生事件:捕获阶段
2. 子元素 原生事件:捕获阶段
3. 父元素 React事件:捕获
4. 子元素 React事件:捕获
5. 子元素 React事件:冒泡
6. 父元素 React事件:冒泡
7. 子元素 原生事件:冒泡阶段
8. document 原生事件:冒泡阶段
4. 主要区别
4.1. 事件委托机制
- React事件是委托到
root节点
统一管理 - 原生事件是直接绑定到DOM元素上
4.2. 事件对象
function HandleEvent({ onClick }) {
const handleClick = (e) => {
console.log('React合成事件对象:', e); // SyntheticEvent
console.log('原生事件对象:', e.nativeEvent); // 原生Event
// React 17之后,e.persist()不再需要
// 事件对象可以被异步访问
setTimeout(() => {
console.log('异步访问事件对象:', e);
}, 0);
};
return <button onClick={handleClick}>点击</button>;
}
5. 阻止事件传播
function StopPropagation() {
const handleParentClick = (e) => {
console.log('父元素被点击');
};
const handleChildClick = (e) => {
e.stopPropagation(); // 阻止React事件传播
e.nativeEvent.stopImmediatePropagation(); // 阻止原生事件传播
console.log('子元素被点击');
};
return (
<div onClick={handleParentClick}>
<button onClick={handleChildClick}>点击我</button>
</div>
);
}
6. 实际应用场景
function Modal({ onClose }) {
const handleBackdropClick = (e) => {
// 确保只有点击背景时才关闭
if (e.target === e.currentTarget) {
onClose();
}
};
const handleContentClick = (e) => {
// 阻止事件冒泡,避免触发背景点击
e.stopPropagation();
};
return (
<div className="backdrop" onClick={handleBackdropClick}>
<div className="modal-content" onClick={handleContentClick}>
模态框内容
</div>
</div>
);
}
7. 注意事项
7.1. 在使用原生事件时,记得在组件卸载时清理
useEffect(() => {
const handler = () => console.log('原生事件');
element.addEventListener('click', handler);
return () => element.removeEventListener('click', handler);
}, []);
8. React 17 前后的事件委托变化
- 事件不再绑定到
document
,而是绑定到root节点
- 这使得多个 React 版本共存成为可能
9. 性能考虑
- 优先使用 React 的合成事件系统
- 只在特殊情况下使用原生事件(如需要捕获特定的键盘事件)
10. React 的事件系统实现原理
10.1. 事件委托(Event Delegation)
React 实现了一个统一的事件系统,采用事件委托的方式:
- 所有事件都绑定到 document 上(在 React 17 之后改为绑定到 root 容器上)
- 当事件触发时,React 会找到对应的组件,然后执行相应的事件处理函数
- 这种方式可以提高性能,减少内存占用
10.2. 合成事件(SyntheticEvent)
React 自己实现了一套事件机制,叫做 SyntheticEvent
(合成事件):
// React 事件示例
const Button = () => {
const handleClick = (e) => {
// e 是 SyntheticEvent 对象,而不是原生的 event
console.log(e);
}
return <button onClick={handleClick}>点击</button>
}
合成事件的特点:
- 抹平了浏览器之间的兼容性差异
- 对事件进行统一的处理和优化
- 符合 W3C 规范
- 可以通过
e.nativeEvent
访问原生事件对象
10.3. 事件注册和触发流程
10.3.1. 事件注册阶段
- React 在初始化时,会在 root 容器上注册所有需要的事件(如 click、change 等)
- 所有事件都会被存储在一个 Map 中进行统一管理
- 通过 JSX 中声明的事件,会存储在组件的 fiber 节点中
10.3.2. 事件触发阶段
- 事件触发时,首先生成合成事件对象
- 按照事件传播的特性(捕获 -> 目标 -> 冒泡)遍历节点
- 调用对应的事件处理函数
10.4. 事件池(Event Pool)
在 React 17 之前:
- React 维护了一个事件池,用于==存放事件对象==
- 事件处理完成后,事件对象会被清空并放回池中重用
- 这就是为什么在==异步操作中无法访问事件对象==
// React 16 及之前版本
function handleClick(e) {
// ❌ 这样无法工作
setTimeout(() => {
console.log(e.target.value); // 此时 e 已被清空
}, 100);
// ✅ 需要手动持久化
e.persist();
setTimeout(() => {
console.log(e.target.value); // 现在可以工作了
}, 100);
}
React 17 之后:
- 移除了事件池机制
- 事件对象可以在异步操作中被访问
- 提升了开发体验
10.5. 执行顺序
React 事件和原生事件的执行顺序:
- 原生事件捕获阶段
- React 事件捕获阶段
- React 事件冒泡阶段
- 原生事件冒泡阶段
// 事件执行顺序示例
<div onClick={e => console.log('原生事件:冒泡')}
onClickCapture={e => console.log('原生事件:捕获')}>
<button
onClick={e => console.log('React事件:冒泡')}
onClickCapture={e => console.log('React事件:捕获')}>
点击
</button>
</div>
10.6. React 的合成事件与原生事件之间存在一些重要的交互特性
- 当同一个 DOM 同时绑定了原生事件和合成事件时,原生事件会先触发
- 在原生事件中调用 stopPropagation() 会阻止合成事件的执行
- 这是因为合成事件依赖事件冒泡到 document(或 root)节点来实现事件委托