React 事件与原生事件执行顺序
React 合成事件与原生事件的执行顺序及版本差异
问题
React 合成事件和原生事件的执行顺序是什么?阻止事件冒泡会有什么影响?
解答
为什么需要合成事件
React 通过合成事件系统统一处理事件,主要有两个原因:
- 抹平浏览器兼容性差异,提供统一的事件对象
- 通过事件委托统一管理事件,可以区分事件优先级,优化用户体验
基础概念
事件委托:通过给父元素绑定事件,利用 event.target 获取触发元素,统一管理子元素事件。
事件监听:使用 addEventListener 可以给同一事件绑定多个处理函数,而直接赋值会覆盖:
// 可以监听多个,不会被覆盖
eventTarget.addEventListener('click', () => {});
eventTarget.addEventListener('click', () => {});
// 第二个会覆盖第一个
eventTarget.onclick = function () {};
eventTarget.onclick = function () {};
事件执行顺序:捕获阶段 => 目标阶段 => 冒泡阶段
React 16 的执行顺序
在 React 16 中,事件绑定在 document 上:
import React, { useRef, useEffect } from "react";
const logFunc = (target, isSynthesizer, isCapture = false) => {
const info = `${isSynthesizer ? "合成" : "原生"}事件,${
isCapture ? "捕获" : "冒泡"}阶段,${target}元素执行了`;
console.log(info);
};
export default function App() {
const divDom = useRef();
const h1Dom = useRef();
useEffect(() => {
const divClickCapFunc = () => logFunc("div", false, true);
const divClickBubFunc = () => logFunc("div", false);
const h1ClickCapFunc = () => logFunc("h1", false, true);
const h1ClickBubFunc = () => logFunc("h1", false);
divDom.current.addEventListener("click", divClickCapFunc, true);
divDom.current.addEventListener("click", divClickBubFunc, false);
h1Dom.current.addEventListener("click", h1ClickCapFunc, true);
h1Dom.current.addEventListener("click", h1ClickBubFunc, false);
}, []);
return (
<div ref={divDom} onClick={() => logFunc("div", true)}>
<h1 ref={h1Dom} onClick={() => logFunc("h1", true)}>
点击我
</h1>
</div>
);
}
点击 h1 时的执行顺序:
- 原生事件捕获阶段:div -> h1
- 原生事件冒泡阶段:h1 -> div
- 合成事件冒泡阶段:h1 -> div(在 document 冒泡时执行)
阻止事件传播的影响
原生事件阻止传播:会阻断后续所有事件(包括合成事件)
const divClickCapFunc = (e) => {
e.stopPropagation(); // 阻止捕获阶段传播
logFunc("div", false, true);
};
合成事件阻止传播:不会影响原生事件,但会阻止后续合成事件
<div onClick={(e) => {
e.stopPropagation(); // 只阻止合成事件
logFunc("div", true);
}}>
React 16 的问题案例
const Modal = ({ onClose }) => {
useEffect(() => {
document.addEventListener('click', onClose);
return () => {
document.removeEventListener('click', onClose);
};
}, [onClose]);
return (
<div onClick={(e) => e.stopPropagation()}>
Modal 内容
</div>
);
};
点击 Modal 内容仍会触发 onClose,因为合成事件的 stopPropagation 无法阻止 document 上的原生事件。
解决方法:使用 e.nativeEvent.stopImmediatePropagation(),它会阻止当前节点上所有事件监听函数的执行。
React 17 的改进
React 17 将事件绑定从 document 改为渲染的根节点(root):
<div id="root">
<div id="app">
<div id="div">
<h1 id="h1">点击我</h1>
</div>
</div>
</div>
执行顺序变为:
- 合成事件捕获阶段:div -> h1
- 原生事件捕获阶段:div -> h1
- 原生事件冒泡阶段:h1 -> div
- 合成事件冒泡阶段:h1 -> div
在 React 17 中,上述 Modal 问题可以直接用 e.stopPropagation() 解决,因为事件不会冒泡到 document。
关键点
- React 16 先执行原生事件,冒泡到 document 时统一执行合成事件
- React 17 在原生事件前后分别执行合成事件的捕获和冒泡阶段,事件绑定在根节点而非 document
- 原生事件阻止传播会阻断合成事件执行,合成事件阻止传播也会影响后续原生事件
stopImmediatePropagation会阻止当前节点所有事件监听,stopPropagation只阻止事件流传播- React 17 的改进避免了多个 React 应用共存时的事件冲突问题
目录