App下載

關(guān)于 useState 的一切

猿友 2020-09-11 14:48:43 瀏覽數(shù) (4351)
反饋

以下文章來源于公眾號:魔術(shù)師卡頌 ,作者卡頌

作為 React 開發(fā)者,你能答上如下兩個問題么:

  1. 對于如下函數(shù)組件:

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

調(diào)用window.updateNum(1)可以將視圖中的0更新為1么?

  1. 對于如下函數(shù)組件:

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

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

  
  return <p onClick={increment}>{num}</p>;
}

在1秒內(nèi)快速點擊p5次,視圖上顯示為幾?

1. 可以
2. 顯示為1

其實,這兩個問題本質(zhì)上是在問:

  • useState如何保存狀態(tài)?
  • useState如何更新狀態(tài)?

本文會結(jié)合源碼,講透如上兩個問題。

這些,就是你需要了解的關(guān)于useState的一切。

hook如何保存數(shù)據(jù)

FunctionComponentrender本身只是函數(shù)調(diào)用。

那么在render內(nèi)部調(diào)用的hook是如何獲取到對應(yīng)數(shù)據(jù)呢?

比如:

  • useState獲取state
  • useRef獲取ref
  • useMemo獲取緩存的數(shù)據(jù)

答案是:

每個組件有個對應(yīng)的fiber節(jié)點(可以理解為虛擬DOM),用于保存組件相關(guān)信息。

每次FunctionComponent render時,全局變量currentlyRenderingFiber都會被賦值為該FunctionComponent對應(yīng)的fiber節(jié)點。

所以,hook內(nèi)部其實是從currentlyRenderingFiber中獲取狀態(tài)信息的。

多個hook如何獲取數(shù)據(jù)

我們知道,一個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如何獲取自己的數(shù)據(jù)呢?

答案是:

currentlyRenderingFiber.memoizedState中保存一條hook對應(yīng)數(shù)據(jù)的單向鏈表。

對于如上例子,可以理解為:

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


hookB.next = hookC;


currentlyRenderingFiber.memoizedState = hookA;

當(dāng)FunctionComponent render時,每執(zhí)行到一個hook,都會將指向currentlyRenderingFiber.memoizedState鏈表的指針向后移動一次,指向當(dāng)前hook對應(yīng)數(shù)據(jù)。

這也是為什么React要求hook的調(diào)用順序不能改變(不能在條件語句中使用hook) —— 每次render時都是從一條固定順序的鏈表中獲取hook對應(yīng)數(shù)據(jù)的。

關(guān)于 useState 的一切

useState執(zhí)行流程

我們知道,useState返回值數(shù)組第二個參數(shù)為改變state的方法

在源碼中,他被稱為dispatchAction

每當(dāng)調(diào)用dispatchAction,都會創(chuàng)建一個代表一次更新的對象update

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

對于如下例子

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

  
  function increment() {
    updateNum(num + 1);
  }

  
  return <p onClick={increment}>{num}</p>;
}

調(diào)用updateNum(num + 1),會創(chuàng)建:

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

如果是多次調(diào)用dispatchAction,例如:

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

那么,update會形成一條環(huán)狀鏈表。

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

                          

這條鏈表保存在哪里呢?

既然這條update鏈表是由某個useStatedispatchAction產(chǎn)生,那么這條鏈表顯然屬于該useState hook。

我們繼續(xù)補充hook的數(shù)據(jù)結(jié)構(gòu)。

const hook = {
  // hook保存的數(shù)據(jù)
  memoizedState: null,
  // 指向下一個hook
  next: hookForB
  // 本次更新以baseState為基礎(chǔ)計算新的state
  baseState: null,
  // 本次更新開始時已有的update隊列
  baseQueue: null,
  // 本次更新需要增加的update隊列
  queue: null,
};

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

在計算state時,會將queue的環(huán)狀鏈表剪開掛載在baseQueue最后面,baseQueue基于baseState計算新的state。

在計算state完成后,新的state會成為memoizedState。

update的鏈表

為什么更新不基于memoizedState而是baseState,是因為state的計算過程需要考慮優(yōu)先級,可能有些update優(yōu)先級不夠被跳過。所以memoizedState并不一定和baseState相同。

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

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

調(diào)用window.updateNum(1)可以將視圖中的0更新為1么?

我們需要看看這里的updateNum方法的具體實現(xiàn):

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

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

上文已經(jīng)介紹,調(diào)用dispatchAction的目的是生成update,并插入到hook.queue鏈表中。

既然queue作為預(yù)置參數(shù)已經(jīng)綁定給dispatchAction,那么調(diào)用dispatchAction就步僅局限在FunctionComponent內(nèi)部了。

update的action

第二個問題

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

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

  
  return <p onClick={increment}>{num}</p>;
}

在1秒內(nèi)快速點擊p5次,視圖上顯示為幾?

我們知道,調(diào)用updateNum會產(chǎn)生update,其中傳參會成為update.action

在1秒內(nèi)點擊5次。在點擊第五次時,第一次點擊創(chuàng)建的update還沒進入更新流程,所以hook.baseState還未改變。

那么這5次點擊產(chǎn)生的update都是基于同一個baseState計算新的state,并且num變量也還未變化(即5次update.action(即num + 1)為同一個值)。

所以,最終渲染的結(jié)果為1。

useState與useReducer

那么,如何5次點擊讓視圖從1逐步變?yōu)?呢?

由以上知識我們知道,需要改變baseState或者action。

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

但是我們可以控制action。

action不僅可以傳,也可以傳函數(shù)。

// action為值
updateNum(num + 1);
// action為函數(shù)
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)

可見,當(dāng)傳時,由于我們5次action為同一個值,所以最終計算的newState也為同一個值。

而傳函數(shù)時,newState基于action函數(shù)計算5次,則最終得到累加的結(jié)果。

如果這個例子中,我們使用useReducer而不是useState,由于useReduceraction始終為函數(shù),所以不會遇到我們例子中的問題。

事實上,useState本身就是預(yù)置了如下reduceruseReducer。

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

總結(jié)

通過本文,我們了解了useState的完整執(zhí)行過程。

以上就是W3Cschool編程獅關(guān)于關(guān)于 useState 的一切的相關(guān)介紹了,希望對大家有所幫助。

0 人點贊