You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

为何useEffect添加的document点击事件在Child组件挂载时触发?

React组件挂载时document点击事件触发的原因分析

我有两个组件,父组件App包含一个按钮,点击该按钮会条件渲染Child组件。

App.js

import { useState } from "react";
import Child from "./Child";

function App() {
  const [value, setValue] = useState(false);

  return (
    <div>
      <button onClick={() => setValue(true)}>Mount Child</button>
      {value ? <Child /> : null}
    </div>
  );
}

export default App;

Child.js

import React, { useEffect } from "react";

function Child() {
  const handleClick = () => {
    console.log("hi");
  };

  useEffect(() => {
    document.addEventListener("click", handleClick);

    return () => {
      document.removeEventListener("click", handleClick);
      console.log("unmounting");
    };
  });

  return <div>Child</div>;
}

export default Child;

问题

为何此处通过document.addEventListener("click", handleClick)添加的事件会在Child组件挂载时触发?

点击按钮后的控制台输出:

unmounting 
hi 

由于在React.StrictMode下运行,unmounting的输出可以理解,但我不清楚为何会输出"hi"。


原因解析

核心在于事件冒泡机制加上React StrictMode的双重挂载逻辑:

  1. 点击按钮时,按钮的onClick回调先执行,将value设为true,触发App组件重新渲染,开始挂载Child组件。
  2. 在StrictMode模式下,React为了检测潜在的副作用问题,会执行「挂载组件 → 立即卸载 → 再次挂载」的流程,这就是控制台先输出unmounting的原因。
  3. 第一次挂载Child时,useEffect会立即执行,给document绑定了handleClick点击事件监听。
  4. 但此时按钮的点击事件还没有完成冒泡流程!当按钮的onClick处理完毕后,点击事件会沿着DOM树向上冒泡,最终到达document节点。
  5. 此时handleClick已经被绑定到document上,这个冒泡过来的点击事件就会触发handleClick,输出hi。
  6. 随后StrictMode触发卸载流程,执行useEffect的清理函数,移除事件监听并输出unmounting,之后Child会被再次挂载,但此时点击事件已经结束,所以不会再触发handleClick。

简单来说:按钮的点击事件还在冒泡途中,Child挂载时绑定的document监听刚好“接住”了这个还没走完的事件,所以触发了hi的输出。


内容的提问来源于stack exchange,提问作者daego

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.15 16:42:44