我知道React组件内嵌套函数或回调(例如setTimeout(中存在过时状态的问题。例如,在下面的代码(取自React文档(中,我们看到了过时状态的问题:
"如果您首先单击"显示警报",然后递增计数器警报将显示您单击"显示"时的计数变量警报"按钮">
function Example() {
const [count, setCount] = useState(0);
function handleAlertClick() {
setTimeout(() => {
alert('You clicked on: ' + count);
}, 3000);
}
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
<button onClick={handleAlertClick}>
Show alert
</button>
</div>
);
}
我知道我们可以使用useRef
:创建一个对状态的最新/当前值的可变引用
function Example() {
const [count, setCount] = useState(0);
const stateRef = useRef()
stateRef.current = count
function handleAlertClick() {
setTimeout(() => {
alert('You clicked on: ' + stateRef.current);
}, 3000);
}
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
<button onClick={handleAlertClick}>
Show alert
</button>
</div>
);
}
我的问题是,为什么创建对useRef
变量(stateRef
(的引用可以解决过时状态的问题,为什么可以绕过闭包的问题?当然,通过直接在setTimeout
中引用count
,一旦组件重新呈现count
值,就会更新,因此我们对它的引用将返回最新值。
我很难理解为什么引用stateRef
而不是count
会绕过过时状态的问题,因为从setTimeout
的角度来看,它们都是在同一词汇范围内声明的。
试着想象一个时间线,每次重新渲染组件都会创建一个"快照";并在这个时间线上留下标记。
所谓的";陈旧状态";更经常被称为";"陈旧的闭合";,这更准确地描述了真实的问题。
关闭究竟是如何造成麻烦的?
每次组件重新渲染时,组件功能本身都会从头到尾重新执行,在执行过程中:
- 一路上所有的钩子调用都被调用
- 所有局部变量都被重新初始化
- 并且重新声明组件函数内的所有嵌套函数
- 而且最重要的是,如果这些嵌套函数中的任何一个引用了组成函数中的局部变量,那么这种引用实际上指向"局部变量";"闭合";,或一个";快照";它捕获那些动态创建并存储在内存中的局部变量的当前状态
如果您调用setTimeout
并向其发送回调函数,并且该函数引用了一个局部变量,那么它实际上存储在渲染时间线上的其中一个快照中。
当组件重新渲染时,新的快照会添加到时间线中,并且只有最新的快照是最新的,而回调函数仍在引用过时的快照/闭包。
当setTimeout
决定执行回调时,回调查看它自己的快照/闭包版本,并说";好的,"count"变量是0;没有意识到它已经过时了。
useRef
如何解决问题?
Cusconst ref = useRef()
在重新渲染过程中始终返回相同的对象引用。因此,即使回调查看过时的快照/闭包,它仍然会看到与最新快照中相同的对象ref
。由于每次重新执行组件函数都会将ref.value = someValue
属性设置为最新值,因此回调获得了访问最新值的方法。
当然,只要直接在setTimeout内引用count组件重新渲染计数值将更新,因此我们引用它将返回最新的值。
不,当您单击按钮时,函数useSetTimeout
使用了当时手头的回调。并且count
不是对您的变量的引用。当组件重新调用时,回调根本不知道您更改了值。
useRef
的不同之处在于它返回一个对象,对象的工作方式与其他类型的变量有点不同。因为它总是同一个对象,所以你总是要处理同一个变量。
顺便说一句,当你打setCount
时,一定要一直打setCount(prevState => ...)
。否则,不能保证您拥有最新的价值。