React 事件与原生事件执行顺序

React 合成事件与原生事件的执行顺序及版本差异

问题

React 合成事件和原生事件的执行顺序是什么?阻止事件冒泡会有什么影响?

解答

为什么需要合成事件

React 通过合成事件系统统一处理事件,主要有两个原因:

  1. 抹平浏览器兼容性差异,提供统一的事件对象
  2. 通过事件委托统一管理事件,可以区分事件优先级,优化用户体验

基础概念

事件委托:通过给父元素绑定事件,利用 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 时的执行顺序:

  1. 原生事件捕获阶段:div -> h1
  2. 原生事件冒泡阶段:h1 -> div
  3. 合成事件冒泡阶段: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>

执行顺序变为:

  1. 合成事件捕获阶段:div -> h1
  2. 原生事件捕获阶段:div -> h1
  3. 原生事件冒泡阶段:h1 -> div
  4. 合成事件冒泡阶段:h1 -> div

在 React 17 中,上述 Modal 问题可以直接用 e.stopPropagation() 解决,因为事件不会冒泡到 document。

关键点

  • React 16 先执行原生事件,冒泡到 document 时统一执行合成事件
  • React 17 在原生事件前后分别执行合成事件的捕获和冒泡阶段,事件绑定在根节点而非 document
  • 原生事件阻止传播会阻断合成事件执行,合成事件阻止传播也会影响后续原生事件
  • stopImmediatePropagation 会阻止当前节点所有事件监听,stopPropagation 只阻止事件流传播
  • React 17 的改进避免了多个 React 应用共存时的事件冲突问题