關於useState的一切

做爲React開發者,你能答上以下兩個問題麼:html

  1. 對於以下函數組件:
function App({
  const [num, updateNum] = useState(0);
  window.updateNum = updateNum;
  return num;
}

調用window.updateNum(1)能夠將視圖中的0更新爲1麼?react

  1. 對於以下函數組件:
function App({
  const [num, updateNum] = useState(0);
  
  function increment({
    setTimeout(() => {
      updateNum(num + 1);
    }, 1000);
  }
  
  return <p onClick={increment}>{num}</p>;
}

在1秒內快速點擊p5次,視圖上顯示爲幾?web

👉向右滑動展現答案                                             1. 能夠
                                                            2. 顯示爲1

其實,這兩個問題本質上是在問:數組

  • useState如何保存狀態?緩存

  • useState如何更新狀態?微信

本文會結合源碼,講透如上兩個問題。數據結構

這些,就是你須要瞭解的關於useState的一切。編輯器

hook如何保存數據

FunctionComponentrender自己只是函數調用。函數

那麼在render內部調用的hook是如何獲取到對應數據呢?flex

好比:

  • useState獲取state

  • useRef獲取ref

  • useMemo獲取緩存的數據

答案是:

每一個組件有個對應的fiber節點(能夠理解爲虛擬DOM),用於保存組件相關信息。

每次FunctionComponent render時,全局變量currentlyRenderingFiber都會被賦值爲該FunctionComponent對應的fiber節點

因此,hook內部實際上是從currentlyRenderingFiber中獲取狀態信息的。

多個hook如何獲取數據

咱們知道,一個FunctionComponent中可能存在多個hook,好比:

function App({
  // hookA
  const [a, updateA] = useState(0);
  // hookB
  const [b, updateB] = useState(0);
  // hookC
  const ref = useRef(0);
  
  return <p></p>;
}

那麼多個hook如何獲取本身的數據呢?

答案是:

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

對於如上例子,能夠理解爲:

const hookA = {
  // hook保存的數據
  memoizedState: null,
  // 指向下一個hook
  next: hookB
  // ...省略其餘字段
};

hookB.next = hookC;

currentlyRenderingFiber.memoizedState = hookA;

FunctionComponent render時,每執行到一個hook,都會將指向currentlyRenderingFiber.memoizedState鏈表的指針向後移動一次,指向當前hook對應數據。

這也是爲何React要求hook的調用順序不能改變(不能在條件語句中使用hook) —— 每次render時都是從一條固定順序的鏈表中獲取hook對應數據的。

useState執行流程

咱們知道,useState返回值數組第二個參數爲改變state的方法

在源碼中,他被稱爲dispatchAction

每當調用dispatchAction,都會建立一個表明一次更新的對象update

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

對於以下例子

function App({
  const [num, updateNum] = useState(0);
  
  function increment({
    updateNum(num + 1);
  }
  
  return <p onClick={increment}>{num}</p>;
}

調用updateNum(num + 1),會建立:

const update = {
  // 更新的數據
  action: 1,
  // 指向下一個更新
  next: null
  // ...省略其餘字段
};

若是是屢次調用dispatchAction,例如:

function increment({
  // 產生update1
  updateNum(num + 1);
  // 產生update2
  updateNum(num + 2);
  // 產生update3
  updateNum(num + 3);
}

那麼,update會造成一條環狀鏈表。

update3 --next--> update1
  ^                 |
  |               update2
  |______next_______|
                          

這條鏈表保存在哪裏呢?

既然這條update鏈表是由某個useStatedispatchAction產生,那麼這條鏈表顯然屬於該useState hook

咱們繼續補充hook的數據結構。

const hook = {
  // hook保存的數據
  memoizedState: null,
  // 指向下一個hook
  next: hookForB
  // 本次更新以baseState爲基礎計算新的state
  baseState: null,
  // 本次更新開始時已有的update隊列
  baseQueue: null,
  // 本次更新須要增長的update隊列
  queue: null,
};

其中,queue中保存了本次更新update的鏈表。

在計算state時,會將queue的環狀鏈表剪開掛載在baseQueue最後面,baseQueue基於baseState計算新的state

在計算state完成後,新的state會成爲memoizedState

爲何更新不基於memoizedState而是baseState,是由於state的計算過程須要考慮優先級,可能有些update優先級不夠被跳過。因此memoizedState並不必定和baseState相同。更詳細的解釋見React技術揭祕[1]

回到咱們開篇第一個問題:

function App({
  const [num, updateNum] = useState(0);
  window.updateNum = updateNum;
  return num;
}

調用window.updateNum(1)能夠將視圖中的0更新爲1麼?

咱們須要看看這裏的updateNum方法的具體實現:

updateNum === dispatchAction.bind(null, currentlyRenderingFiber, queue);

可見,updateNum方法即綁定了currentlyRenderingFiberqueue(即hook.queue)的dispatchAction

上文已經介紹,調用dispatchAction的目的是生成update,並插入到hook.queue鏈表中。

既然queue做爲預置參數已經綁定給dispatchAction,那麼調用dispatchAction就步僅侷限在FunctionComponent內部了。

update的action

第二個問題

function App({
  const [num, updateNum] = useState(0);
  
  function increment({
    setTimeout(() => {
      updateNum(num + 1);
    }, 1000);
  }
  
  return <p onClick={increment}>{num}</p>;
}

在1秒內快速點擊p5次,視圖上顯示爲幾?

咱們知道,調用updateNum會產生update,其中傳參會成爲update.action

在1秒內點擊5次。在點擊第五次時,第一次點擊建立的update還沒進入更新流程,因此hook.baseState還未改變。

那麼這5次點擊產生的update都是基於同一個baseState計算新的state,而且num變量也還未變化(即5次update.action(即num + 1)爲同一個值)。

因此,最終渲染的結果爲1。

useState與useReducer

那麼,如何5次點擊讓視圖從1逐步變爲5呢?

由以上知識咱們知道,須要改變baseState或者action

其中baseStateReact的更新流程決定,咱們沒法控制。

可是咱們能夠控制action

action不只能夠傳,也能夠傳函數

// action爲值
updateNum(num + 1);
// action爲函數
updateNum(num => num + 1);

在基於baseStateupdate鏈表生成新state的過程當中:

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

// 遍歷baseQueue中的每個update
do {
  if (typeof update.action === 'function') {
    newState = update.action(newState);
  } else {
    newState = action;
  }
while (update !== firstUpdate)

可見,當傳時,因爲咱們5次action爲同一個值,因此最終計算的newState也爲同一個值。

而傳函數時,newState基於action函數計算5次,則最終獲得累加的結果。

若是這個例子中,咱們使用useReducer而不是useState,因爲useReduceraction始終爲函數,因此不會遇到咱們例子中的問題。

事實上,useState自己就是預置了以下reduceruseReducer

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

總結

經過本文,咱們瞭解了useState的完整執行過程。

本系列文章接下來會繼續以實例 + 源碼的方式,解讀業務中常常使用的React特性。

點擊閱讀原文,開源電子書輕鬆學懂React源碼

參考資料

[1]

React技術揭祕: https://react.iamkasong.com/state/priority.html#%E4%BB%80%E4%B9%88%E6%98%AF%E4%BC%98%E5%85%88%E7%BA%A7


本文分享自微信公衆號 - 牧碼的星星(gh_0d71d9e8b1c3)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索