【问题标题】:setTimeout for this.state vs useStatethis.state 与 useState 的 setTimeout
【发布时间】:2021-12-11 16:30:18
【问题描述】:

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

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

当我使用钩子时:

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

如果我触发setTimeout,然后在超时之前将count 更改为1(5000ms),类组件将console.log(1)(最新值),对于useState,它是console.log(0)(值注册超时时)。
为什么会这样?

【问题讨论】:

    标签: javascript reactjs react-hooks


    【解决方案1】:

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

    【讨论】:

      【解决方案2】:

      更新版本:

      问题:对于 functionclass 组件,setTimeout / setInterval 内的 React State 变量的行为有何不同? p>

      案例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)
      }
      

      案例 6班级获胜(无需额外努力避免过时的关闭)。但是,在函数组件中如何避免

      // 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)
        }
      }, [])
      

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

      【讨论】:

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

      超时不能很好地与 react 声明式编程模型配合使用。在功能组件中,每次渲染都是一个时间帧。他们从不改变。 状态更新时,所有状态变量都在本地重新创建,不会覆盖旧的关闭变量。

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

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

      Dan Abramov 有一个 fantastic article 解释所有这一切和一个解决这个问题的钩子。正如您正确回答的那样,问题是由陈旧的关闭引起的。解决方案确实涉及使用 refs。

      【讨论】:

        【解决方案4】:

        说明

        对于函数组件,每个渲染都是一个函数调用,为该特定调用创建一个新的函数闭包。函数组件正在关闭 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>
          );
        }
        

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

        【讨论】:

          猜你喜欢
          • 2020-06-21
          • 2020-08-18
          • 2020-11-19
          • 1970-01-01
          • 2018-12-09
          • 1970-01-01
          • 2020-02-22
          相关资源
          最近更新 更多