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后执行,这种情况不该发生。
原因解析
- 事件触发顺序是核心:点击按钮时,先触发按钮的
onClick回调更新counter状态,但document.body的点击事件还没执行——因为React的合成事件要冒泡到document才会触发原生事件。而状态更新会触发组件重新渲染,这个过程中React会先执行上一轮useEffect的清理函数,再执行新的useEffect添加新的事件监听。 - 闭包记住了旧状态:
- 第一次渲染时,useEffect添加的监听函数记住了当时的
counter=0; - 点击按钮后,
setCounter让counter变成1,组件开始重新渲染; - 重新渲染前,React先执行上一轮的清理函数,输出
cleaner; - 新渲染完成后,新的useEffect添加了一个记住
counter=1的监听函数; - 这时按钮的点击事件才冒泡到document,两个监听函数都会被触发:旧的输出
0,新的输出1。
- 第一次渲染时,useEffect添加的监听函数记住了当时的
- 添加依赖项后为何依旧异常:就算把
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
相关产品推荐
相关产品推荐

