iT邦幫忙

0

React hooks 的基礎概念:hooks鏈表

nero 2021-02-14 21:23:011745 瀏覽

當函數組件進入render階段時,會被renderWithHooks函數處理。函數組件作為壹個函數,它的渲染其實就是函數調用,而函數組件又會調用React提供的hooks函數。初始掛載和更新時,所用的hooks函數是不同的,比如初次掛載時調用的useEffect,和後續更新時調用的useEffect,雖然都是同壹個hook,但是因為在兩個不同的渲染過程中調用它們,所以本質上他們兩個是不壹樣的。這種不壹樣來源於函數組件要維護壹個hooks的鏈表,初次掛載時要創建鏈表,後續更新的時候要更新鏈表。

分屬於兩個過程的hook函數會在各自的過程中被賦值到ReactCurrentDispatcher的current屬性上。所以在調用函數組件之前,當務之急是根據當前所處的階段來決定ReactCurrentDispatcher的current,這樣才可以在正確的階段調用到正確的hook函數。

export function renderWithHooks<Props, SecondAwrg>(
  current: Fiber | null,
  workInProgress: Fiber,
  Component: (p: Props, arg: SecondArg) => any,
  props: Props,
  secondArg: SecondArg,
  nextRenderLanes: Lanes,
): any {
  
  // 區分是掛載還是更新過程,獲取不同的hooks函數集合
  ReactCurrentDispatcher.current =
        current === null || current.memoizedState === null
          ? HooksDispatcherOnMount
          : HooksDispatcherOnUpdate;
  // 調用函數組件,
  let children = Component(props, secondArg);
  ...
  return children;
}

HooksDispatcherOnMountHooksDispatcherOnUpdate,它們內部的hooks函數是不同的實現,區別之壹在於不同階段對於hooks鏈表的處理是不同的。

const HooksDispatcherOnMount: Dispatcher = {
  useCallback: mountCallback,
  useContext: readContext,
  useEffect: mountEffect,
  ...
};

const HooksDispatcherOnUpdate: Dispatcher = {
  useCallback: updateCallback,
  useContext: readContext,
  useEffect: updateEffect,
  ...
};

認識hooks鏈表

無論是初次掛載還是更新,每調用壹次hooks函數,都會產生壹個hook對象與之對應。以下是hook對象的結構。

{
    baseQueue: null,
    baseState: 'hook1',
    memoizedState: null,
    queue: null,
    next: {
        baseQueue: null,
        baseState: null,
        memoizedState: 'hook2',
        next: null
        queue: null
    }
}

產生的hook對象依次排列,形成鏈表存儲到函數組件fiber.memoizedState上。在這個過程中,有壹個十分重要的指針:workInProgressHook,它通過記錄當前生成(更新)的hook對象,可以間接反映在組件中當前調用到哪個hook函數了。每調用壹次hook函數,就將這個指針的指向移到該hook函數產生的hook對象上。例如:

const HooksExp = () => {
    const [ stateHookA, setHookA ] = useState('A')
    useEffect(() => { console.log('B') })
    const [ stateHookC, setHookC ] = useState('C')
    return <div>Hook Example</div>
}

上面的例子中,HooksExp組件內壹共調用了三個hooks函數,分別是useState、useEffect、useState。那麽構建hook鏈表的過程,可以概括為下面這樣,重點關註workInProgressHook的指向變化。

調用useState('A'):

fiber.memoizedState: hookA
                       ^
                workInProgressHook

調用useEffect:

fiber.memoizedState: hookA -> hookB
                                ^
                         workInProgressHook

調用useState('C'):

fiber.memoizedState: hookA -> hookB -> hookC
                                         ^
                                 workInProgressHook

hook函數每次執行,都會創建它對應的hook對象,去進行下壹步的操作,比如useReducer會在hook對象上掛載更新隊列,useEffect會在hook對象上掛載effect鏈表。而創建hook對象的過程實際上也是hooks鏈表構建以及workInProgressHook指針指向更新的過程。

組件掛載

初次掛載時,組件上沒有任何hooks的信息,所以,這個過程主要是在fiber上創建hooks鏈表。掛載調用的是mountWorkInProgressHook,它會創建hook並將他們連接成鏈表,同時更新workInProgressHook,最終返回新創建的hook,也就是hooks鏈表。

function mountWorkInProgressHook(): Hook {
  // 創建hook對象
  const hook: Hook = {
    memoizedState: null,
    baseState: null,
    baseQueue: null,
    queue: null,
    next: null,
  };

  if (workInProgressHook === null) {
    // workInProgressHook為null說明此時還沒有hooks鏈表,
    // 將新hook對象作為第壹個元素掛載到fiber.memoizedState,
    // 並將workInProgressHook指向它。
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
    // workInProgressHook不為null說明已經有hooks鏈表,此時將
    // 新的hook對象連接到鏈表後邊,並將workInProgressHook指向它。
    workInProgressHook = workInProgressHook.next = hook;
  }
  // 返回的workInProgressHook即為新創建的hook
  return workInProgressHook;
}

currentlyRenderingFiber就是workInProgress節點

我們在組件中調用hook函數,就可以獲取到hook對象,例如useState:

const HooksDispatcherOnMount: Dispatcher = {
  ...
  useState: mountState,
  ...
};

function mountState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  // 獲取hook對象
  const hook = mountWorkInProgressHook();

  // 對hook對象的處理
  ...
  return [hook.memoizedState, dispatch];
}

組件更新

在更新過程中,由於存在current樹,所以workInProgress節點也就有對應的current節點。那麽自然也會有兩條hooks鏈表,分別存在於current和workInProgress節點的memorizedState屬性上。鑒於此,更新過程的hooks鏈表構建需要另壹個指針的參與:currentHook。它作為組件的workInProgressHook在上壹次更新時對應的hook對象,新的hook對象可以基於它創建。另外,也可以獲取到上次hook對象的壹些數據,例如useEffect的前後依賴項比較,前壹次的依賴項就可以通過它獲得。

                currentTree

       current.memoizedState = hookA -> hookB -> hookC
                                          ^             
                                      currentHook
                                          |
         workInProgress Tree              |
                                          |                                
workInProgress.memoizedState = hookA -> hookB
                                          ^          
                                 workInProgressHook

所以更新過程的hooks鏈表構建過程除了更新workInProgressHook指針的指向,還要更新currentHook的指向,以及盡可能復用currentHook來創建新的hook對象。
這個過程調用的是updateWorkInProgressHook函數:

function updateWorkInProgressHook(): Hook {
  // 確定nextCurrentHook的指向
  let nextCurrentHook: null | Hook;
  if (currentHook === null) {
    // currentHook在函數組件調用完成時會被設置為null,
    // 這說明組件是剛剛開始重新渲染,剛剛開始調用第壹個hook函數。
    // hooks鏈表為空
    const current = currentlyRenderingFiber.alternate;
    
    if (current !== null) {
      // current節點存在,將nextCurrentHook指向current.memoizedState
      nextCurrentHook = current.memoizedState;
    } else {
      nextCurrentHook = null;
    }
  } else {
    // 這說明已經不是第壹次調用hook函數了,
    // hooks鏈表已經有數據,nextCurrentHook指向當前的下壹個hook
    nextCurrentHook = currentHook.next;
  }
  // 確定nextWorkInProgressHook的指向
  let nextWorkInProgressHook: null | Hook;
  if (workInProgressHook === null) {
    // workInProgress.memoizedState在函數組件每次渲染時都會被設置成null,
    // workInProgressHook在函數組件調用完成時會被設置為null,
    // 所以當前的判斷分支說明現在正調用第壹個hook函數,hooks鏈表為空
    // 將nextWorkInProgressHook指向workInProgress.memoizedState,為null
    nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
  } else {
    // 走到這個分支說明hooks鏈表已經有元素了,將nextWorkInProgressHook指向
    // hooks鏈表的下壹個元素
    nextWorkInProgressHook = workInProgressHook.next;
  }

  if (nextWorkInProgressHook !== null) {
    // 依據上面的推導,nextWorkInProgressHook不為空說明hooks鏈表不為空
    // 更新workInProgressHook、nextWorkInProgressHook、currentHook
    workInProgressHook = nextWorkInProgressHook;
    nextWorkInProgressHook = workInProgressHook.next;

    currentHook = nextCurrentHook;
  } else {
    // 走到這個分支說明hooks鏈表為空
    // 剛剛調用第壹個hook函數,基於currentHook新建壹個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,
    };
    
    // 依據情況構建hooks鏈表,更新workInProgressHook指針
    if (workInProgressHook === null) {
      currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
    } else {
      workInProgressHook = workInProgressHook.next = newHook;
    }
  }
  return workInProgressHook;
}

總結

通過本文我們了解到,函數組件內的hooks函數調用是會形成壹個hooks鏈表的,這個鏈表會掛載到函數組件對應fiber的memoizedState屬性上。這為我們後續hooks函數的講解奠定了基礎。


圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言