React 原理系列 —— Hook 是這樣工作的

語言: CN / TW / HK

Hook 是 React 16.8 的新增特性。它可以讓你在不編寫 class 的情況下使用 state 以及其他的 React 特性。

Part 1 函式式元件和 Hook

通常情況下,我們在函式式元件中這樣呼叫 hook:

function Example() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>add</button>
    </div>
  );
}

函式式元件本身是個純函式,沒有任何狀態,它通過呼叫 useState 獲取一個狀態和改變狀態的方法。但這個 useState 是 React 匯出的“全域性”函式,和當前函式式元件沒有任何顯式關聯。所以必定有某種力量將一個函式式元件例項和它用到的 state 繫結起來。

Fiber 上的 Hook

這個力量就是 Fiber,在 Fiber 節點中有兩個相關屬性:

  1. type:指向 Component,可能是 Function Component,也可能是 Class Component 等其他元件。對 Function Component 來說,就是一個具體的 render 函式,比如上面的 Example 函式。
  2. memoizedState:指向自身狀態,在 Class Fiber 下是建構函式宣告的 state,在 Function Fiber 下則是一個 Hook 池。Hook 池中維護著元件呼叫 useXXX 產生的所有 Hook,Hook 中又分別記著各自的狀態。這樣就實現了 Hook 和 Fiber 的繫結。
Hook 池和 Fiber 繫結

Hook 池和 Fiber 繫結的意義在於,當某個 Function Component 的 Fiber 開始 render,它能根據狀態池定位到上一次 render 的 Hook 狀態,為本次 render 執行的所有 useXXX 提供行為依據,“一次性”函式就有了“延續”的狀態。

找到“上一次”

任意一個 Function Component Fiber 更新時都會走到 renderWithHooks 方法:

function updateFunctionComponent( current, workInProgress, Component, nextProps, renderExpirationTime ) {
  nextChildren = renderWithHooks(current,workInProgress,Component, nextProps,context,renderExpirationTime);
}

這個方法在 ReactFiberHooks 模組中,模組裡有全域性的 nextCurrentHook 指標,表明當前指向的 Hook。renderWithHooks 會首先切換 nextCurrentHook 到當前 Fiber 的 Hook 池,再執行 render 函式,然後 render 函式中呼叫的所有“全域性”useXXX 都從這個指標獲取“上一次”。

切換 nextCurrentHook
function renderWithHooks(current,workInProgress,Component,props,refOrContext,nextRenderExpirationTime) {
  // 切換 nextCurrentHook
  nextCurrentHook = current !== null ? current.memoizedState : null;
  // 執行 render 方法
  let children = Component(props, refOrContext);
  return children;
}

弄清楚 Hook 和 Fiber 的繫結和切換,接下來我們進到一個 Fiber 節點內部,看看 Hook 池的維護機制。

Part 2 Hook 池的維護機制

Hook 機制的所有實現,都在前面提到的 ReactFiberHooks 模組中。

Hook 的資料結構

Hook 是一個物件,render 中呼叫 useXXX 方法,就會建立一個 Hook 物件。

type Hook = {
  memoizedState: any;
  baseState: any;
  baseUpdate: Update<any, any> | null;
  queue: UpdateQueue<any, any> | null;
  next: Hook | null;
}

如果你呼叫的方法不一樣,Hook 物件裡面的欄位搭載的資訊也不一樣。比如 useState、useReducer 這樣的 State Hook,和 useEffect 這樣的 Effect Hook,就會在 memoizedState 上存不同的東西。

當元件呼叫多次 useXXX,就會建立多個 Hook。同一 Component 的多個 Hook 之間用連結串列連線起來,構成 Hook 池,Fiber 的 memoizedState 就指向池中第一個 Hook:

Hook 池結構

除了 nextCurrentHook,ReactFiberHooks 提供了一些其他指標來做遍歷:

let currentHook: Hook | null = null;
let nextCurrentHook: Hook | null = null;
let firstWorkInProgressHook: Hook | null = null;
let workInProgressHook: Hook | null = null;
let nextWorkInProgressHook: Hook | null = null;

前兩個指標用於遍歷已有的 Hook 池,後三個指標用來構建一個新 Hook 池。

方法換檔

雖然看起來 Function Component 每次 render 都呼叫的同一個 useXXX,但實際上 mount 和 update 呼叫對 Hook 池是幾乎完全不同的操作。

因此 ReactFiberHooks 提供了一個換檔機制:宣告兩套 HooksDispatcher,上面綁定了 mount、update 階段不同的 Hook 實現。當一個 Function Component 準備 render 時,判斷它是 mount 還是 update,切換不同的 HooksDispatcher。

換擋機制

具體的換擋邏輯仍在 renderWithHooks 中,而對 mount 還是 update 的判斷,則依賴當前 Fiber 是否有 Hook 池:

nextCurrentHook = current !== null ? current.memoizedState : null;
ReactCurrentDispatcher.current =
      nextCurrentHook === null
        ? HooksDispatcherOnMount
        : HooksDispatcherOnUpdate;

這裡其實不合理,如果元件沒用到 Hook,即使在 update 階段,也不會有 Hook 池。但也無所謂,反正發動機沒轉,換哪個檔都不會動。既然能換擋,我們也就能看到不同階段下,Hook 池的構建和讀取方式。

Hook 池的構建

Fiber mount 階段,Fiber 上沒有 Hook 池,從頭構建:

Hook 池的構建
  1. 構造新 Hook。
  2. 接入連結串列。如果 Fiber 上沒有 Hook,說明當前 Hook 是整個函式式元件的第一個 Hook,放 firstWorkInProgressHook 最後接造 Fiber 上;如果有 Hook,直接 next 接下去。
  3. 更新指標。

這部分邏輯抽象在每個 mountXXX 都要調的 mountWorkInProgressHook 中。

function mountWorkInProgressHook(): Hook {
  // 1. 構造新 Hook
  const hook: Hook = {
    memoizedState: null,
    baseState: null,
    queue: null,
    baseUpdate: null,
    next: null,
  };
  // 2. 接入連結串列;3. 更新指標
  if (workInProgressHook === null) {
    firstWorkInProgressHook = workInProgressHook = hook;
  } else {
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}

mountWorkInProgressHook 返回建立的 Hook,隨後不同的 mountXXX 會再對 Hook 做各自的處理,後面分開說。

等整個 Hook 池構建完畢,在 renderWithHooks 中掛到 Fiber 的 memoizedState 上:

// renderWithHooks 方法
renderedWork.memoizedState = firstWorkInProgressHook;

Hook 池的更新

Fiber 開始 update,通過 renderWithHooks 讀取 Fiber.memoizedState 上的第一個 Hook,並給到 nextCurrentHook 指標。

nextCurrentHook = current !== null ? current.memoizedState : null;

然後每個逐個進入 updateXXX。我們以第一個 Hook 操作為例,會調通用的 updateWorkInProgressHook 方法執行以下操作:

  1. 克隆當前 Hook。為什麼不直接複用?這樣可以保證 Fiber 上的 Hook “原件”完整,某些情況下(比如 useEffect 要對比新舊 Hook 的依賴),在構建當前 Hook 的同時,仍需要上一次 Hook 的資訊。
  2. 更新指標。沿著 next 把“當前 Hook”指標指向連結串列的下一次節點,同時 workInProgressHook 也通過 next 構建下去。這就是網紅問題 “為什麼 Hook 不能用在 if / for 語句裡?” 的答案,一旦中間某次 useXXX 沒 next,會導致後續所有 Hook 取錯。

附部分程式碼:

function updateWorkInProgressHook() {
  currentHook = nextCurrentHook;
  // 1. 克隆 Hook
  const newHook: Hook = {
    memoizedState: currentHook.memoizedState,
    baseState: currentHook.baseState,
    queue: currentHook.queue,
    baseUpdate: currentHook.baseUpdate,
    next: null,
  };
  // 2. 更新指標
  if (workInProgressHook === null) {
    workInProgressHook = firstWorkInProgressHook = newHook;
  } else {
    workInProgressHook = workInProgressHook.next = newHook;
  }
  nextCurrentHook = currentHook.next;
  return workInProgressHook;
}

等 Hook 池更新完畢,renderWithHooks 同樣會把 Fiber 的 memoizedState 切換到 firstWorkInProgressHook。

小結

這節介紹了 Hook 的基本機制:

  • 函式內的 Hook 呼叫建立一個 Hook 物件,上面儲存著狀態資料。
  • Hook 維護在一個 Hook 池中,並掛到 Fiber 節點上,Hook 池是一個單向連結串列。
  • Fiber 在 mount 和 update 階段通過“換擋”切換 dispatcher,呼叫不同的 useXXX 實現。
  • mount 階段要構建 Hook 池;update 階段則逐個克隆 Hook,構建新的 Hook 池。

Part 3 State Hook:提供狀態

State Hook 來自 useState、useReducer,用來給函式式元件提供狀態,它的實現圍繞狀態及其更新。

const [ state, dispatch ] = useState(initial);

State Hook 結構

在 State Hook 中,memoizedState 儲存著 Hook 的最新狀態,baseState 則是初始狀態(useState 傳入)。queue 儲存著這個 hook 的更新佇列,資料結構為單向迴圈連結串列:

State Hook 構建:mount 階段

State Hook 經過通用的構造階段,執行自己的邏輯,我們補充到下圖中:

  • 狀態初始賦值。也就是我們呼叫 useState 的入參。
  • 構造更新佇列,繫結 dispatch。更新佇列會在需要更新的時候被“清洗”來計算最新 memoizedState,而 dispatch 是修改佇列的唯一入口。
  • 返回 state 和 dispatch。這樣我們第一次呼叫 useState 完成,獲得 Hook 的初始狀態值和更新狀態的方法。

附程式碼:

function mountState() {
  const hook = mountWorkInProgressHook();
  // 4. 狀態初始賦值
  hook.memoizedState = hook.baseState = initialState;
  // 5. 構造更新佇列
  const queue = (hook.queue = { last, dispatch, lastRenderedReducer, lastRenderedState });
  const dispatch = (queue.dispatch = (dispatchAction.bind(
    null,
    ((currentlyRenderingFiber: any): Fiber),
    queue,
  )));
  // 6. 返回 state 和 dispatch
  return [hook.memoizedState, dispatch];
}

dispatchAction 在 mount 階段繫結到 Hook 上並返回,後續的更新直接來 Hook 上調就好。

State Hook 讀取:update 階段

Fiber update 階段,useState 從 Hook 池中克隆出 Hook,獲取歷史狀態,計算新狀態。以第一個 Hook 操作為例,補齊後續幾步:

  • 計算最新 state。這是個清理 Update Queue 的動作,可選,如果兩次 render 之間沒有對這個 Hook 節點的 set 操作,就不會有 Update,可以直接返回現有 state。
  • 返回最新 state 和 dispatch。

附部分程式碼:

function updateReducer() {
  const hook = updateWorkInProgressHook();
  const queue = hook.queue;
  // 3. 合併 queue,計算最新 memoizedState
  let newState = hook.memoizedState;
  let update = firstRenderPhaseUpdate;
  do {
    const action = update.action;
    newState = reducer(newState, action);
    update = update.next;
  } while (update !== null);
  hook.memoizedState = newState;
  const dispatch: Dispatch<A> = (queue.dispatch: any);
  // 4. 
  return [hook.memoizedState, dispatch];
}

State Hook 更新:dispatchAction

dispatchAction 是第一次調 useState 時返回出去的,作為 setXXX 呼叫。那很顯然,setXXX 把傳入的更新加入 Hook 更新佇列,並觸發一次更新。這裡借用之前那篇的圖:( React Fiber 架構 —— “更新”到底是個啥 - 知乎

很簡單的插入連結串列操作,完成後通過 scheduleWork 發起更新。

程式碼:

function dispatchAction<S, A>(fiber: Fiber, queue: UpdateQueue<S, A>, action: A) {
  // 1. 構造更新
  const update: Update<S, A> = {
    expirationTime,
    action,
    eagerReducer: null,
    eagerState: null,
    next: null,
  };
  // 2. 插入佇列
  const last = queue.last;
  if (last === null) {
    update.next = update;
  } else {
    const first = last.next;
    if (first !== null) {
      update.next = first;
    }
    last.next = update;
  }
  queue.last = update;
  // 3. 發起排程
  scheduleWork(fiber, expirationTime);
}

小結

這節介紹了 Stete Hook 的實現:

  • State Hook 來自 useState、useReducer,用來提供狀態及其更新。
  • State Hook 通過 memoizedState 儲存狀態,通過 queue 維護更新佇列的資料和方法(dispatch)。
  • State Hook 的更新佇列是個單向迴圈連結串列。
  • 更新階段的 State Hook 會“清洗”更新佇列,計算並返回最新 memoizedState。
  • 我們呼叫 useState 返回的 dispatch,就是建立並在更新佇列中插入新更新,併發起整體排程。

Part 4 Effect Hook:依賴監聽和清理

Effect Hook 來自 useEffect、useLayoutEffect,為函式式元件提供依賴監聽。

useEffect(() => {
    // create Effect
  return () => {
    // destroy Effect
  };
}, [deps]);

Effect Hook 結構

在 Effect Hook 上,只有 memoizedState 被用到,用來儲存一個 Effect 物件。這個物件會被插入 Fiber 的更新佇列,告訴 Fiber 更新後要做哪些動作。

type Effect = {
  tag: HookEffectTag,
  create: () => (() => void) | void,
  destroy: (() => void) | void,
  deps: Array<mixed> | null,
  next: Effect,
};
  • create:就是我們傳入的第一個回撥函式,在依賴變化的時候執行
  • destroy:用來記錄 create 執行後的返回函式。某些階段來自於上一次同一個 Effect Hook 的 create 執行結果,某些階段來自於自身。
  • deps:就是我們傳入的依賴陣列,用來進行依賴對比。
  • tag:決定 Effect 在 Fiber 提交時如何被處理。這個很關鍵,它的值來自於 useEffect 的執行時機和依賴變化情況。

Effect Hook 構建和更新

第一次渲染,走到 mountEffect,通過通用 mountWorkInProgressHook 構造並插入一個 Hook 返回。然後對這個 Hook 做一些操作,我們關注 4、5、6 步:

  • 構造 Effect(副作用物件)。記錄了副作用的依賴陣列、回撥函式、清理函式、處理方式。
  • Effect 加入 Fiber 節點的 updateQueue。updateQueue 是個連結串列,連結串列元素就是我們構造的 Effect,這個連結串列會在 Fiber 更新完畢後逐個根據 Effect 物件提供的資訊處理和清空。
  • Effect 同時也會掛到當前 Hook 上。

構建程式碼:

function mountEffectImpl(fiberEffectTag, hookEffectTag, create, deps): void {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  hook.memoizedState = pushEffect(hookEffectTag, create, undefined, nextDeps);
}
function pushEffect(tag, create, destroy, deps) {
  const effect: Effect = {...};
  const lastEffect = componentUpdateQueue.lastEffect;
  if (lastEffect === null) {
    componentUpdateQueue.lastEffect = effect.next = effect;
  } else {
    const firstEffect = lastEffect.next;
    lastEffect.next = effect;
    effect.next = firstEffect;
    componentUpdateQueue.lastEffect = effect;
  }
  return effect;
}

更新執行函式式元件時,每個 useEffect 又會被分別執行一次,同樣要分別克隆 Hook,然後構造 Effect 掛到 updateQueue 和 Hook 上。

  • 對比舊 Hook,確定 Effect 物件的屬性。這是因為 Effect 的內容要依賴前面的 Effect,比如銷燬函式(destroy)就是由上一次執行建立函式(create)返回的。下一節我們展開看。
  • 構造 Effect
  • Effect 入隊
  • Effect 掛到當前 Hook 上

程式碼:

function updateEffectImpl(fiberEffectTag, hookEffectTag, create, deps): void {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  let destroy = undefined;
  if (currentHook !== null) {
    // 4. 對比舊 Hook,確定 Effect 物件的屬性
    // ...暫時省略,後面展開說
  }
  hook.memoizedState = pushEffect(hookEffectTag, create, destroy, nextDeps);
}

至此,無論 Fiber 初始化還是更新,我們所有 useEffect 產生的 Effect,都可以從 Hook 池和 Update Queue 訪問到。其中 Hook 池方便我們管理所有 Hook(包括之前 State Hook)的資料,Effect 就是 Effect Hook 的資料;Update Queue 則為了方便 Fiber 直接拿到 Effect 執行副作用。

Effect 屬性的取值和流轉

接下來我們看看 Effect 在構造過程中,在不同場景下,是如何根據使用者傳參、Effect 之間關係,來確定自身屬性的。

Effect 由 pushEffect 函式建立,傳入的四個引數直接賦值給對應屬性:

function pushEffect(tag, create, destroy, deps) {
  const effect: Effect = {
    tag,
    create,
    destroy,
    deps,
  };
  // ...
}

pushEffect 的呼叫入參,直接決定了 Effect 的內容。

初始化建立 Effect

在 mountEffectImpl 中,是對 Effect 的初始化建立: pushEffect(hookEffectTag, create, undefined, nextDeps) ,對應屬性傳值如下:

  • tag:也就是 hookEffectTag,值為 mountEffect 傳入的 UnmountPassive | MountPassive => 0b10000000 | 0b01000000 => 0b11000000
  • create:useEffect 的第一個入參函式
  • destroy:傳入一個 undefined,因為初始化建立的 Effect 不存在“前一次執行”
  • deps:依賴陣列

Effect 被執行

然後初始化建立的 Effect 會在 Fiber commit 的時候被執行,在 commitWork 階段一個叫 commitHookEffectList 的方法中,對 Effect 做了這樣的動作:

const create = effect.create;
effect.destroy = create();

這時 Effect 的 destroy 被「暫時」掛上了自己 create 的返回。這就是前面說的「某些階段來自於上一次同一個 Effect Hook 的 create 執行結果,某些階段來自於自身。」

但這樣不會亂嗎?上一次 destroy 不就找不到了嗎?不會。因為 Effect 總是先執行上一次 destroy,再執行自己的 create,此時上一次 destroy 已經執行過沒用了,正好空出來 destroy 屬性掛自己的,方便傳給下一次 Effect。

更新建立 Effect

Fiber 更新階段再執行 useEffect,來到 updateEffectImpl。這裡首先會去拿上一次構造的 Effect,再看這張圖:

新 Hook 被克隆出來,上一次 Hook 則留在 currentHook 指標上,通過 currentHook.memoizedState 拿到它的 Effect,以及Effect 的依賴(deps)和銷燬函式(destroy,按前面所說,此時得到的 destroy 就是上一次 Effect 自己的)。

接著會做一件重要的事: 依賴對比 ,依賴是否變化,對當前 Effect 的處理方式影響很大。

依賴變化是怎麼對比出來的?遍歷 + 淺比較。

// areHookInputsEqual 方法
for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
  if (is(nextDeps[i], prevDeps[i])) {
    continue;
  }
  return false;
}
return true;

// is 方法
return (x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y);

當依賴有變化,建立的 Effect 屬性值如下:

  • tag:也就是 hookEffectTag,值為 updateEffect 傳入的 UnmountPassive | MountPassive => 0b10000000 | 0b01000000 => 0b11000000
  • create:useEffect 的第一個入參函式
  • destroy:上一次 Effect 的 destroy
  • deps:依賴陣列

當依賴沒變化,Effect tag 會變成 NoHookEffect => 0b00000000

小結

至此我們摸清了在被 Fiber commit 處理前,Update Queue 中的 Effect 屬性來源:

  • tag:需要處理的時候(mount 或依賴有變的 update)為 0b11000000 ,不需要處理的時候(依賴不變的 update)為 0b00000000
  • create:useEffect 的第一個入參函式
  • destroy:上一次 Effect 的 destroy
  • deps:依賴陣列

處理 Fiber 上的 Effect

useEffect 產生的所有 Effect 都加入 Fiber 的 Update Queue,由 Fiber 在 commit 階段統一處理。

在 commitRoot 入口,對 FunctionComponent 這樣呼叫 commitHookEffectList 方法:

commitHookEffectList(UnmountPassive, NoHookEffect, finishedWork);
commitHookEffectList(NoHookEffect, MountPassive, finishedWork);

其中前兩個引數是常量 tag,用來和 Effect tag 比較判斷 Effect 的處理方式,對應方法引數 unmountTag、mountTag;第三個引數 finishedWork 傳入當前 Fiber。commitHookEffectList 被先後調了兩次,從傳參來看,NoHookEffect 是 0b00000000 置空,兩次的 UnmountPassive(0b10000000) 和 MountPassive(0b01000000) 分別啟用 unmountTag、mountTag,依次執行 umount 和 mount 操作。

commitHookEffectList 是統一處理 Update Queue 中 Effect 的入口,再回顧下 Update Queue 的結構:

佇列裡第一個 Effect 可以通過 const firstEffect = finishedWork.updateQueue.lastEffect.next 拿到,然後按照單向迴圈連結串列遍歷:

// 遍歷 Update Queue
do {
  // 處理 Effect
  effect = effect.next;
} while (effect !== firstEffect);

Unmount 處理

處理 Effect 的方式判斷很巧妙,通過一個二進位制位運算: (effect.tag & unmountTag) !== NoHookEffect ,判斷是否需要進行 umount 處理。NoHookEffect 是個 0,那就要 effect.tag 和 unmountTag 不同且都不為 0,條件判斷才為 true。結合 Effect 的屬性值和 commitHookEffectList 傳參內容,我們列舉出以下幾種情況:

  • Effect 依賴沒變(effect.tag = 0),則肯定不做 unmount 處理
  • 第二次 commitHookEffectList(unmountTag = 0),也肯定不做 ummount 處理
  • Effect 依賴有變(effect.tag = 0b11000000),且第一次 commitHookEffectList(unmountTag = 0b10000000),則做 unmount 處理:執行 effect.destroy(上一次 Effect 的銷燬函式)
if ((effect.tag & unmountTag) !== NoHookEffect) {
  const destroy = effect.destroy;
  effect.destroy = undefined;
  if (destroy !== undefined) {
    destroy();
  }
}

Mount處理

也通過二進位制來做: (effect.tag & mountTag) !== NoHookEffect

  • Effect 依賴沒變(effect.tag = 0),則肯定不做 mount 處理
  • 第一次 commitHookEffectList(mountTag = 0),也肯定不做 mount 處理
  • Effect 依賴有變(effect.tag = 0b11000000),且第二次 commitHookEffectList(mountTag = 0b01000000),則做 mount 處理:執行 effect.create(回撥 useEffect 入參,並把返回作為 destroy 掛到 Effect 上)
if ((effect.tag & mountTag) !== NoHookEffect) {
  const create = effect.create;
  effect.destroy = create();
}

小結

這節介紹了 Effect Hook 實現:

  • Effect Hook 來自 useEffect、useLayoutEffect。
  • Effect Hook 通過 memoizedState 儲存一個 useEffect 產生的 Effect 物件。
  • Effect 物件儲存著建立(create)回撥、銷燬(destroy)回撥、依賴、處理標記。
  • Effect 會同時掛到 Fiber 的 Update Queue 上,方便 Fiber 在 commit 階段找到並執行,Update Queue 是個單向迴圈連結串列。
  • 更新階段,Effect 會找到同一呼叫在上一次構建的 Effect,對比依賴以決定被如何處理,並獲取 destroy。

Part 5 其他 Hook

經過前面對 Hook 池和兩種關鍵 Hook 的介紹,我們基本摸清了 Hook 的套路:呼叫 useXXX —> 構建 Hook 維護 Hook 池 —> 在 Hook 上加一些不同 useXXX 特有的資料和邏輯。以此類推,就很容易猜到其他 Hook 的實現方式了。

useMemo

useMemo 返回一個memoized 值。把“建立”函式和依賴項陣列作為引數傳入 useMemo,它僅會在某個依賴項改變時才重新計算 memoized 值。

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

Memo Hook 上要存什麼狀態?依賴值和觸發的計算值,實現上是一個數組。然後把計算值返回出去:

// mountMemo 和 updateMemo 方法
const nextValue = nextCreate();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;

但在 update 階段,上述邏輯是有條件的,即“依賴有變化”,否則只要返回上一次計算值就好。所以 useEffect 曾用到的 areHookInputsEqual 又出場了:

// updateMemo 方法
const prevState = hook.memoizedState;
if (areHookInputsEqual(nextDeps, prevDeps)) {
  return prevState[0];
}
const nextValue = nextCreate();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;

useCallback

同 useMemo 十分相似,不同點是入參的方法不執行,變為直接儲存:

// mountMemo 和 updateMemo 方法
hook.memoizedState = [callback, nextDeps];
return nextValue;

useRef

useRef 就像是可以在其 .current 屬性中儲存一個可變值的“盒子”,而這個“盒子”的引用值始終不變。

只要在 mount 時建立一個物件,存到 Hook 上,後續 update 直接取:

// mountRef
const ref = {current: initialValue};
hook.memoizedState = ref;
return ref;

// updateRef
return hook.memoizedState;

Part Z 總結

本篇介紹了 Hook 的實現原理:

  • Function Component 中每次呼叫 useXXX,都會建立一個 Hook,這些 Hook 以 Hook 池的形式維護在 Fiber.memoizedState 上。
  • Hook 是一個物件,上面存著對應呼叫的資料。不同 useXXX 方法在 Hook 上存的東西也不同。
  • Function 初次執行和後續更新執行,Hook 池的維護方式、useXXX 要做的事大不相同,所以 React 會判斷當前 Fiber 所處階段(mount 或 update),換擋呼叫不同的 useXXX 實現。
  • Fiber mount 時,Hook 被依次從頭建立;update 時則逐個從舊 Hook 池中克隆,構造成新 Hook 池後再切換 Fiber.memoizedState,這樣能保留舊 Hook,提供“新舊 Hook 對比”的能力。
  • State Hook(useState、useReducer)在 Hook 物件上儲存狀態值和更新佇列,update 階段會清理更新佇列並計算最新狀態值。useState、useReducer 返回的 dispatch 方法用來把更新加入佇列,併發起一次更新排程。
  • Effect Hook(useEffect)在 Hook 物件上儲存 Effect,一個記錄副作用建立、銷燬、依賴的物件。update 階段構建新的 Effect,並對比新老 Hook 上 Effect 的依賴,決定 Effect 處理方式。Effect 會被同時記錄在 Fiber 自身的更新佇列裡,等待 Fiber commit 後統一處理。