【REACT HOOKS】useState是如何保存並更新數據的?


在項目中,我們通常會使用useState來初始化並更新數據。如下:

function App(){
  const [num, setNum] = useState(0);

  function increment() {
    setTimeout(() => {
      setNum(num + 1);
    }, 1000);
  }
  
  return <button onClick={increment}>{num}</button>;
}

num初始化為0,點擊按鈕進行加一操作。但是在以上代碼中,如果用戶在一秒內點擊五次按鈕,最后依然會顯示1。

為什么呢?這就不得不聊聊useState是如何工作的了。

hook如何保存數據

在react中通過currentRenderingFiber來標識當前渲染節點,每個組件都有一個對應的fiber節點,用來保存組件的相關數據信息。
每次函數組件渲染時,currentRenderingFiber就被賦值為當前組件對應的fiber,所以實際上hook是通過currentRenderingFiber來獲取狀態信息的。

多個hook如何獲取數據

react hook允許我們在一個組件中多次使用hook。如下:

function App(){
      const [num, setNum] = useState(0);
      const [name, setName] = useState('ashen');
      const [age, setAge] = useState(21);
}

currentRenderingFiber.memorizedState中保存一條hook對應數據的單向鏈表。

const hookNum = {
      memorizedState: null,
      next: hookName
}
hookName.next = hookAge;
currentRenderingFiber.memorizedState.next = hookNum;

當函數組件渲染時,每執行到一個hook,就會將currentRenderingFiber.memorizedState的指針向后移一下。這也是hook的調用順序不能改變的原因(不能再條件語句中使用hook)

hook如何更新數據

使用useState時,返回值數組的第二個參數是用來更新數據的,稱為dispatchAction.
每當調用dispatchAction時,都會創建一個update對象:

const update = {
      // 更新數據
      action: action,
      // 指向下一個更新
      next: null
}

當我們多次更新state時,會形成一條環式更新鏈表
在以上例子中,如果點擊按鈕多次進行自增操作

function increment() {
  // 產生update1
  updateNum(num + 1);
  // 產生update2
  updateNum(num + 2);
  // 產生update3
  updateNum(num + 3);
}
update3 --next--> update1
  ^                 |
  |               update2
  |______next_______|

這些dispatchAction產生的update對象也會保存在hook當中

const hook = {
      memorizedState: null,
      next: null,
      baseState: null,
      baseQueue: null,
      queue: null
}

其中queue中保存了本次更新的鏈表,baseQueue中保存了本次更新開始時已有的鏈表,在計算state時,會基於baseState進行更新操作。在計算state完成后,新的state就會成為memorizedState。
為什么更新不基於memoizedState而是baseState,是因為state的計算過程需要考慮優先級,可能有些update優先級不夠被跳過。所以memoizedState並不一定和baseState相同。

現在我們回到最開始的那個問題,在一秒內點擊五次按鈕進行更新,但是在這五次更新生成的時候,第一次的更新還沒有進行,所以baseState並未改變,都是基於0進行更新,點擊五次后依然是1。

那么如何解決這個問題呢?實際上,更新state的函數不只可以傳值,還可以傳函數。

function increment() {
    setTimeout(() => {
      setNum(num => num + 1);
    }, 1000);
  }

在基於baseState和update更新state時:

let newState = baseState;
let firstUpdate = hook.baseQueue.next;
let update = firstUpdate;

if(typeof update === 'function'){
      newState = update.action(newState)
}else {
    newState = action;
}

如上可見,當傳入值時,由於5次action都是同一個值,所以結果為1.
當傳入函數時,每次更新都是基於上一次更新后的值進行改變,所以點擊五次會變為5。

而上例中,如果使用的是useReducer,由於第二個參數action只能是函數,所以不會產生上述問題。
useState實際上就是內置了如下reducer的useReducer

function basicStateReducer(state, action){
      return typeof action === 'function' ? action(state) : action;
}

可以用下圖來概括本文內容


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM