banner
IWSR

IWSR

我永远喜欢志喜屋梦子!

React Hooks: hooks 鏈表

文章的 React 版本基於 v17.02 的 concurrent 模式

hooks 鏈表是在講解所有 hook 前繞不開的話題,此文將以調試的角度出發分析 hooks 鏈表的結構與作用。

元件掛載時的鏈表邏輯#

首先我們試著對這段程式碼進行錄製。

  import { useLayoutEffect, useEffect, useState } from "react";

  function UseEffectAnduseLayoutEffect() {
    const [text, setText] = useState('wawawa');

    useEffect(() => {
      console.log('useEffect create 1');
      // setText('useEffect create 1');

      return () => {
        console.log('useEffect destroy 1');
        // setText('useEffect destroy 1');
      }
    }, []);

    useLayoutEffect(() => {
      console.log('useLayoutEffect create 1');
      // setText('useLayoutEffect create 1');

      return () => {
        console.log('useLayoutEffect destroy 1');
        // setText('useLayoutEffect destroy 1');
      }
    }, []);

    return (
      <div onClick={() => { setText('hahaha') }}>{ text }</div>
    )
  }

  export default UseEffectAnduseLayoutEffect;

image

從結果中我們不難注意到 renderWithHooks 這一函數,該函數內執行了 UseEffectAnduseLayoutEffect,函數式元件作為函數被調用本身就是渲染的一環,除此之外在這個函數中也調用了代碼中的 hook。

export function renderWithHooks<Props, SecondArg>(
  current: Fiber | null,
  workInProgress: Fiber,
  Component: (p: Props, arg: SecondArg) => any,
  props: Props,
  secondArg: SecondArg,
  nextRenderLanes: Lanes,
): any {
  ...
  ReactCurrentDispatcher.current =
    current === null || current.memoizedState === null
      ? HooksDispatcherOnMount
      : HooksDispatcherOnUpdate;

  let children = Component(props, secondArg); // 此處只需要關注調用UseEffectAnduseLayoutEffect的場景
  ...
  return children;
}

在 React 內根據是否存在 current 來判斷是掛載還是更新是一個比較常見的操作。由於我們目前記錄的是初次掛載時的操作,因此此時 ReactCurrentDispatcher.current 為 HooksDispatcherOnMount 。至於為什麼需要區分掛載 / 更新,後續會介紹。

接下來我們關注在 UseEffectAnduseLayoutEffect 內調用的 useState 與 useEffect 。我們將進入構築 hooks 鏈表的相關邏輯。

在 UseEffectAnduseLayoutEffect 這一例子中最先被調用的是 useState ,而對其進行斷點調試會進入 mountState 。

function mountState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  const hook = mountWorkInProgressHook();
  if (typeof initialState === 'function') {
    // $FlowFixMe: Flow doesn't like mixed types
    initialState = initialState();
  }
  hook.memoizedState = hook.baseState = initialState;
  const queue = (hook.queue = {
    pending: null,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: (initialState: any),
  });
  const dispatch: Dispatch<
    BasicStateAction<S>,
  > = (queue.dispatch = (dispatchAction.bind(
    null,
    currentlyRenderingFiber,
    queue,
  ): any));
  return [hook.memoizedState, dispatch];
}

但是在其代碼中我們目前只需要關注 mountWorkInProgressHook ,這是用來構建 hook 對象的

每調用一次 hooks 函數都會創建一個 hook 對象,其結構如下:

function mountWorkInProgressHook(): Hook {
  const hook: Hook = {
    memoizedState: null,

    baseState: null,
    baseQueue: null,
    queue: null,

    next: null,
  }; // hook對象的結構 是構成 hooks鏈表的最小單元

  if (workInProgressHook === null) {
    // This is the first hook in the list
    /**
     * 這裡可以看到,fiber 中的 memoizedState 是用來存儲 hooks 鏈表的
    */
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
    // Append to the end of the list
    /**
     * 後續在調用其他 hook 時,會直接接入到上一个 hook 對象的後面
    */
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}

上述代碼中請注意 workInProgressHook 這一指針,它反映了當前元件中調用了哪一個 hook 函數。

當代碼中的 useState、useEffect、useLayout 全部執行完之後我們會獲取到一條完整的 hooks 鏈表。

image

此時也可以看到 workInProgressHook 確實指向了最後一個 hook 對象
image

元件更新時的鏈表邏輯#

錄製一下觸發點擊事件的記錄,得到以下的結果。

image

此時 ReactCurrentDispatcher.current 為 HooksDispatcherOnUpdate。因此再次進入 UseEffectAnduseLayoutEffect 時調用的 hooks 將與掛載時不同。此時再次對 useState 打斷點將進入 updateState 內的 updateWorkInProgressHook

function updateWorkInProgressHook(): Hook {
  let nextCurrentHook: null | Hook;
  // 根據註釋 current hook list is the list that belongs to the current fiber 可知,current hook 指向了 current fiber 樹上的 hooks 鏈表
  // 畢竟更新操作時,必然已經存在了 current 樹
  // currentHook在函數元件調用完成時會被設置為null因此可以用來確認是否是剛開始重新渲染
  if (currentHook === null) {
    /**
     * 根據註釋 The work-in-progress fiber. I've named it differently to distinguish it from the work-in-progress hook.
     * currentlyRenderingFiber是一個 WIP 的 fiber
     * 因此它的 alternate 是指向其對應的 current fiber 節點
    */
    const current = currentlyRenderingFiber.alternate;
    if (current !== null) {
      // current節點存在,將nextCurrentHook指向current.memoizedState(hooks 鏈表)
      nextCurrentHook = current.memoizedState;
    } else {
      nextCurrentHook = null;
    }
  } else {
    // 指針後移
    nextCurrentHook = currentHook.next;
  }

  let nextWorkInProgressHook: null | Hook;
  if (workInProgressHook === null) {
    nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
  } else {
    nextWorkInProgressHook = workInProgressHook.next;
  }

  if (nextWorkInProgressHook !== null) {
    // There's already a work-in-progress. Reuse it.
    workInProgressHook = nextWorkInProgressHook;
    nextWorkInProgressHook = workInProgressHook.next;

    currentHook = nextCurrentHook;
  } else {
    // Clone from the current hook.

    invariant(
      nextCurrentHook !== null,
      'Rendered more hooks than during the previous render.',
    );
    currentHook = nextCurrentHook;

    const newHook: Hook = {
      memoizedState: currentHook.memoizedState,

      baseState: currentHook.baseState,
      baseQueue: currentHook.baseQueue,
      queue: currentHook.queue,

      next: null,
    };
    
    if (workInProgressHook === null) {
      // This is the first hook in the list.
      currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
    } else {
      // Append to the end of the list.
      workInProgressHook = workInProgressHook.next = newHook;
    }
  }
  return workInProgressHook;
}

代碼冗長,指針又跳來跳去,看懂的最好辦法還是自己調試一下。這裡最重要的信息其實就是,在更新時會借 current 節點上的 hooks 鏈表去生成新的 hooks 鏈表。至於如何更新狀態不在本文內介紹。

總結#

  1. 每次調用函數式元件都會生成一個 hooks 鏈表掛載在對應 fiber 的 memoizedState 上。
載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。