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

React中useEffect绑定document点击事件的控制台输出疑惑

React 点击按钮后控制台输出异常问题解析

问题代码

import { useEffect, useState } from "react";

function App() {
  const [counter, setCounter] = useState(0);
  useEffect(() => {
    const listener = () => console.log(counter);
    // 不要这么做
    document.body.addEventListener("click", listener);
    const cleanUp = () => console.log("cleaner");
    return cleanUp;
  });

  return (
    <div>
      <button onClick={() => setCounter(counter + 1)}>+ Increment</button>
      <div>Count: {counter}</div>
    </div>
  );
}

export default App;

问题现象

首次点击按钮时,控制台输出如下:

cleaner 
0
1

按预期,首次点击按钮触发状态变更,应该只输出cleaner和counter为0时的事件结果,但实际多输出了1。

补充:即使将counter作为useEffect的依赖项,首次点击按钮时仍会输出1,按常理useEffect在counter从0更新到1后执行,这种情况不该发生。

原因解析

  1. 事件触发顺序是核心:点击按钮时,先触发按钮的onClick回调更新counter状态,但document.body的点击事件还没执行——因为React的合成事件要冒泡到document才会触发原生事件。而状态更新会触发组件重新渲染,这个过程中React会先执行上一轮useEffect的清理函数,再执行新的useEffect添加新的事件监听。
  2. 闭包记住了旧状态:
    • 第一次渲染时,useEffect添加的监听函数记住了当时的counter=0;
    • 点击按钮后,setCounter让counter变成1,组件开始重新渲染;
    • 重新渲染前,React先执行上一轮的清理函数,输出cleaner;
    • 新渲染完成后,新的useEffect添加了一个记住counter=1的监听函数;
    • 这时按钮的点击事件才冒泡到document,两个监听函数都会被触发:旧的输出0,新的输出1。
  3. 添加依赖项后为何依旧异常:就算把counter加到useEffect的依赖里,事件触发顺序没变——点击事件的冒泡是在状态更新、清理旧监听、添加新监听之后才发生的,所以旧监听(记住0)和新监听(记住1)都还绑定在body上,都会执行,自然还是输出0和1。

解决方案

要解决这个问题,必须在清理函数中移除对应的事件监听,避免旧监听残留:

import { useEffect, useState } from "react";

function App() {
  const [counter, setCounter] = useState(0);
  useEffect(() => {
    const listener = () => console.log(counter);
    document.body.addEventListener("click", listener);
    // 清理时移除当前的监听函数
    const cleanUp = () => {
      console.log("cleaner");
      document.body.removeEventListener("click", listener);
    };
    return cleanUp;
  }, [counter]); // 添加counter作为依赖,确保监听函数捕获最新的counter值

  return (
    <div>
      <button onClick={() => setCounter(counter + 1)}>+ Increment</button>
      <div>Count: {counter}</div>
    </div>
  );
}

export default App;

这样每次清理时都会移除旧的监听,点击事件只会触发当前有效的监听函数,就不会出现重复输出的情况。

内容的提问来源于stack exchange,提问作者Scáthach

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.29 23:07:40