关于JavaScript闭包运行机制及React代码输出异常的技术咨询
嘿,我来帮你拆解这个问题!你这段React代码里,定时器每次打印的都是0 1而非count和increment的当前值,核心问题出在闭包的变量捕获特性,再加上对useEffectEvent的用法误解,咱们一步步理清楚:
问题根源分析
闭包的作用域捕获
组件首次渲染时,useEffect的依赖是空数组[],所以只会执行一次。调用sI()时,定时器的回调函数会捕获当前作用域下的count(初始值0)和increment(初始值1)。
后续组件重新渲染时,虽然count和increment的值会更新,但它们是新的变量(每个渲染周期都会创建新的状态变量),而定时器回调依然绑定在首次渲染的作用域上,所以始终打印初始的0 1。useEffectEvent的用法误区useEffectEvent的设计初衷是把需要访问最新状态的逻辑与effect的依赖解耦,但你把整个定时器逻辑都塞进了useEffectEvent里,这并没有改变闭包捕获的本质——这个函数依然是在首次渲染时创建的,内部的count和increment还是初始值。
解决方案
方案一:用useRef保存最新状态值
通过useRef来存储count和increment的最新引用,确保定时器能访问到实时值:
import { useState, useEffect, useRef } from 'react'; export default function Timer() { const [count, setCount] = useState(0); const [increment, setIncrement] = useState(1); // 用ref保存最新状态 const countRef = useRef(count); const incrementRef = useRef(increment); // 状态更新时同步ref的值 useEffect(() => { countRef.current = count; }, [count]); useEffect(() => { incrementRef.current = increment; }, [increment]); useEffect(() => { const id = setInterval(() => { setCount(c => c + incrementRef.current); // 打印最新值 console.log(countRef.current, incrementRef.current); }, 1000); return () => clearInterval(id); }, []); return ( <> <h1> Counter: {count} <button onClick={() => setCount(0)}>Reset</button> </h1> <hr /> <p> Every second, increment by: <button disabled={increment === 0} onClick={() => { setIncrement(i => i - 1); }}>–</button> <b>{increment}</b> <button onClick={() => { setIncrement(i => i + 1); }}>+</button> </p> </> ); }
方案二:正确使用useEffectEvent
把需要访问最新状态的逻辑抽离到useEffectEvent中,让effect里的定时器只负责触发逻辑,而非捕获状态:
import { useState, useEffect } from 'react'; import { experimental_useEffectEvent as useEffectEvent } from 'react'; export default function Timer() { const [count, setCount] = useState(0); const [increment, setIncrement] = useState(1); // 用useEffectEvent包裹需要访问最新状态的逻辑 const getLatestValues = useEffectEvent(() => { console.log(count, increment); return increment; // 返回最新的增量值 }); useEffect(() => { const id = setInterval(() => { // 调用getLatestValues获取最新状态 setCount(c => c + getLatestValues()); }, 1000); return () => clearInterval(id); }, []); return ( <> <h1> Counter: {count} <button onClick={() => setCount(0)}>Reset</button> </h1> <hr /> <p> Every second, increment by: <button disabled={increment === 0} onClick={() => { setIncrement(i => i - 1); }}>–</button> <b>{increment}</b> <button onClick={() => { setIncrement(i => i + 1); }}>+</button> </p> </> ); }
这样每次定时器触发时,getLatestValues都会拿到当前最新的count和increment,因为useEffectEvent会自动绑定最新的组件状态,而effect本身不需要添加任何依赖。
备注:内容来源于stack exchange,提问作者Vaibhav

