繁体   English   中英

setTimeout for this.state vs useState

[英]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 这将始终使用最新的计数值。
检查此链接以获取更多信息。

更新后的版本:

问题: functionclass组件的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)
}

案例 6Class 获胜(无需额外努力避免过时关闭)。 但是,如何在 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)
  }
}, [])

源 1 , 源 2

案例 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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM