[英]setTimeout for this.state vs useState
当我使用 class 组件时,我有代码:
setTimeout(() => console.log(this.state.count), 5000);
当我使用钩子时:
const [count, setCount] = useState(0);
setTimeout(() => console.log(count), 5000);
如果我触发setTimeout
然后在超时( 5000ms
)之前将count
更改为 1 , class 组件将console.log(1)
(最新值),对于useState
它是console.log(0)
(注册超时时的值)。
为什么会这样?
对于useState
,它在第一次使用count
创建超时。 它通过closure
访问count
数值。 当我们通过setCount
设置新值时,组件会重新渲染,但不会更改传递给 timeout 的值。
我们可以使用const count = useRef(0)
并传递给 timeout count.current
。 这将始终使用最新的计数值。
检查此链接以获取更多信息。
更新后的版本:
问题: function和class组件的setTimeout
/ setInterval
内的 React State 变量的行为差异?
案例 1 : function 组件中的 State 变量(过时关闭):
const [value, setValue] = useState(0)
useEffect(() => {
const id = setInterval(() => {
// It will always print 0 even after we have changed the state (value)
// Reason: setInterval will create a closure with initial value i.e. 0
console.log(value)
}, 1000)
return () => {
clearInterval(id)
}
}, [])
案例 2 : class 组件中的 State 变量(无陈旧关闭):
constructor(props) {
super(props)
this.state = {
value: 0,
}
}
componentDidMount() {
this.id = setInterval(() => {
// It will always print current value from state
// Reason: setInterval will not create closure around "this"
// as "this" is a special object (refernce to instance)
console.log(this.state.value)
}, 1000)
}
案例 3 :让我们尝试围绕this
创建一个陈旧的闭包
// Attempt 1
componentDidMount() {
const that = this // create a local variable so that setInterval can create closure
this.id = setInterval(() => {
console.log(that.state.value)
// This, too, always print current value from state
// Reason: setInterval could not create closure around "that"
// Conclusion: Oh! that is just a reference to this (attempt failed)
}, 1000)
}
案例 4 :让我们再次尝试在 class 组件中创建一个陈旧的闭包
// Attempt 2
componentDidMount() {
const that = { ...this } // create a local variable so that setInterval can create closure
this.id = setInterval(() => {
console.log(that.state.value)
// Great! This always prints 0 i.e. the initial value from state
// Reason: setInterval could create closure around "that"
// Conclusion: It did it because that no longer is a reference to this,
// it is just a new local variable which setInterval can close around
// (attempt successful)
}, 1000)
}
案例 5 :让我们再次尝试在 class 组件中创建一个陈旧的闭包
// Attempt 3
componentDidMount() {
const { value } = this.state // create a local variable so that setInterval can create closure
this.id = setInterval(() => {
console.log(value)
// Great! This always prints 0 i.e. the initial value from state
// Reason: setInterval created closure around value
// Conclusion: It is easy! value is just a local variable so it will be closed
// (attempt successful)
}, 1000)
}
案例 6 : Class 获胜(无需额外努力避免过时关闭)。 但是,如何在 function 组件中避免它?
// Let's find solution
const value = useRef(0)
useEffect(() => {
const id = setInterval(() => {
// It will always print the latest ref value
// Reason: We used ref which gives us something like an instance field.
// Conclusion: So, using ref is a solution
console.log(value.current)
}, 1000)
return () => {
clearInterval(id)
}
}, [])
案例 6 :让我们为 function 组件寻找另一种解决方案
useEffect(() => {
const id = setInterval(() => {
// It will always print the latest state value
// Reason: We used updater form of setState (which provides us latest state value)
// Conclusion: So, using updater form of setState is a solution
setValue((prevValue) => {
console.log(prevValue)
return prevValue
})
}, 1000)
return () => {
clearInterval(id)
}
}, [])
原始版本:
该问题是由闭包引起的,可以使用ref
修复。 但这里有一个解决方法,即使用setState
的“updater”形式访问最新的state
值:
function App() { const [count, setCount] = React.useState(0); React.useEffect(() => { setTimeout(() => console.log('count after 5 secs: ', count, 'Wrong'), 5000) }, []) React.useEffect(() => { setTimeout(() => { let count setCount(p => { console.log('p: ', p) count = p return p }) console.log('count after 5 secs: ', count, 'Correct') }, 5000); }, []) return (<div> <button onClick={() => setCount(p => p+1)}>Click me before 5 secs</button> <div>Latest count: {count}</div> </div>) } ReactDOM.render(<App />, document.getElementById('mydiv'))
<script crossorigin src="https://unpkg.com/react@17/umd/react.development.js"></script> <script crossorigin src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script> <body> <div id="mydiv"></div> </body>
超时不能很好地与反应声明式编程 model 配合使用。 在功能组件中,每次渲染都是一个时间帧。 他们从不改变。 当 state 更新时,所有 state 变量都在本地重新创建,并且不会覆盖旧的关闭变量。
您也可以以相同的方式考虑效果,其中效果将在其本地 realm 中运行,其所有本地 state 变量在每个渲染上,新渲染不会影响其 Z78E6221F6393D1356681DB398F14CED6
摆脱这种 model 的唯一方法是参考文献。 或 class 组件,其中 state 实际上类似于实例( this
)是 ref 容器的 refs。 Refs 允许交叉渲染通信和关闭破坏。 谨慎使用。
Dan Abramov 有一篇很棒的文章解释了这一切,还有一个可以解决这个问题的钩子。 正如您正确回答的那样,问题是由陈旧的关闭引起的。 解决方案确实涉及使用 refs。
使用 function 组件,每个渲染都是一个 function 调用,为该特定调用创建一个新的 function 闭包。 function 组件正在关闭 setTimeout 回调 function,因此 setTimeout 回调中的所有内容都只能访问调用它的特定渲染。
使用 Ref 并仅在 setTimeout 回调中访问它将为您提供一个跨渲染持久的值。
然而,使用一个总是更新的值的 React Ref 并不方便,比如一个计数器。 你负责更新值,并自己重新渲染。 更新 Ref 并不需要组件渲染。
为了便于使用,我的解决方案是将 useState 和 useRef 挂钩组合成一个“useStateAndRef”挂钩。 这样,您将获得一个获取值的 setter,以及在 setTimeout 和 setInterval 等异步情况下使用的 ref:
import { useState, useRef } from "react";
function useStateAndRef(initial) {
const [value, setValue] = useState(initial);
const valueRef = useRef(value);
valueRef.current = value;
return [value, setValue, valueRef];
}
export default function App() {
const [count, setCount, countRef] = useStateAndRef(0);
function logCountAsync() {
setTimeout(() => {
const currentCount = countRef.current;
console.log(`count: ${count}, currentCount: ${currentCount}`);
}, 2000);
}
return (
<div className="App">
<h1>useState with updated value</h1>
<h2>count: {count}</h2>
<button onClick={() => setCount(prev => prev + 1)}>+</button>
<button onClick={logCountAsync}>log count async</button>
</div>
);
}
工作代码沙盒链接: https://codesandbox.io/s/set-timeout-with-hooks-fdngm?file=/src/App.tsx
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.