this.state 与 useState 的 setTimeout

IT技术 javascript reactjs react-hooks
2021-03-15 13:40:26

当我使用类组件时,我有代码:

setTimeout(() => console.log(this.state.count), 5000);

当我使用钩子时:

const [count, setCount] = useState(0);
setTimeout(() => console.log(count), 5000);

如果我触发setTimeout然后count在超时 ( 5000ms)之前将更改为 1 ,类组件将console.log(1)(最新值),并且useStateconsole.log(0)(注册超时时的值)。
为什么会发生这种情况?

4个回答

对于useState,它会count在第一次使用时创建超时count通过 a访问该closure当我们通过 设置新值时setCount,组件会重新渲染但不会更改传递给 timeout 的值。
我们可以使用const count = useRef(0)并传递给 timeout count.current这将始终使用最新的计数值。
查看此链接以获取更多信息。

更新后的版本:

问:差的国家作出react变量中的行为setTimeout/setInterval功能一流的组件?

案例 1:函数组件中的状态变量(过时的闭包):

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:类组件中的状态变量(没有陈旧的闭包):

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:让我们再次尝试在类组件中创建一个陈旧的闭包

// 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:让我们再次尝试在类组件中创建一个陈旧的闭包

// 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获胜(没有额外的努力来避免过时的关闭)。但是,如何在函数组件中避免它

// 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:让我们为功能组件寻找另一种解决方案

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. 但这里有一个解决方法来修复它,即state使用“更新程序”形式访问最新setState

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>

@deckelesetState(prevValue => prevValue)不会导致重新渲染,因为正在返回相同的值。React 会做:应该渲染:Object.is(oldValue, newValue)=> false所以,如果我们这样做,就不会重新渲染setCount(p => { // do_something; return p})
2021-04-26 13:40:26
文档说如果 setState 的结果与前一个值相同,则没有额外的渲染。然而,这并不完全正确,整个函数被重新渲染第二次,然后才避免渲染它的孩子:github.com/facebook/react/issues/14994尽管如此,你在你的例子中使用 setState 是正确的在 useEffect 中是安全的。但是在render之外使用这个方法会导致无限循环。即使当前值与前一个相同。
2021-05-01 13:40:26
@deckele 我刚看到你的编辑:But using this method outside in render will cause an infinite loop. Even though current value is identical to the previous one而且,我测试过,是的,你是对的。我从来不知道,因为我从来没有写过这样的代码,但你是对的 :) 谢谢!
2021-05-10 13:40:26
@deckel 好的。或许。我说这个的时候没有阅读文档 - there will no re-render if we do setCount(p => { // do_something; return p})我在本地测试了它,然后想到了写它。我不知道真相是什么。在任何情况下,我提供的示例都是为了理解目的(它们很少是真实世界的案例)。除此之外,也许我们少操心无限重新渲染使用setTimeout
2021-05-12 13:40:26
这是一个有趣的解决方案。缺点是使用 setter 获取最新值也会导致重新渲染,因此整个 setTimeout 副作用会导致不必要的渲染。在某些情况下,这可能会导致无限循环,例如,如果 useEffect 取决于更改的计数。
2021-05-19 13:40:26

超时与 reacts 声明式编程模型不能很好地配合。在功能组件中,每个渲染在时间上都是单个帧。他们永远不会改变。当状态更新时,所有状态变量都会在本地重新创建,并且不会覆盖旧的关闭变量。

您也可以以相同的方式考虑效果,其中效果将在其本地领域中运行,每次渲染时都会使用其所有本地状态变量,并且新渲染不会影响其输出。

打破这种模式的唯一方法是 refs。或者类组件,其中 state 有效地类似于 refs,其中实例 ( this) 是 ref 容器。Refs 允许交叉渲染通信和关闭破坏。谨慎使用。

Dan Abramov 有一篇很棒的文章解释了这一切,还有一个钩子可以解决这个问题。正如您正确回答的那样,该问题是由过时的关闭引起的。解决方案确实涉及使用 refs。

解释

对于函数组件,每个渲染都是一个函数调用,为该特定调用创建一个新的函数闭包。函数组件正在关闭 setTimeout 回调函数,因此 setTimeout 回调中的所有内容都只访问调用它的特定渲染。

可重复使用的解决方案:

使用 Ref 并仅在 setTimeout 回调中访问它会给你一个在渲染中持久的值。

然而,使用一个总是更新的值的 React Ref 并不是那么方便,比如计数器。您负责更新值并自行重新渲染。更新 Ref 不需要组件渲染。

为了便于使用,我的解决方案是将 useState 和 useRef 钩子组合成一个“useStateAndRef”钩子。这样,您将获得一个既可以获取值又可以在异步情况下使用的 ref 的 setter,例如 setTimeout 和 setInterval:

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>
  );
}

工作 CodeSandbox 链接:https ://codesandbox.io/s/set-timeout-with-hooks-fdngm ? file =/ src/App.tsx