簡體   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