Основная концепция хуков React: список хуков

React.js

Когда компонент функции входит в стадию рендеринга, он будет обработан функцией renderWithHooks. В качестве функции рендеринг функционального компонента на самом деле является вызовом функции, и функциональный компонент будет вызывать функцию ловушек, предоставляемую React. Во время начального монтирования и обновления используемые хуки-функции различаются. Например, useEffect, вызываемый во время начального монтирования, и useEffect, вызываемый во время последующих обновлений, являются одним и тем же хуком, но поскольку они вызываются во время двух разных процессов рендеринга. это две разные вещи. Это отличие связано с тем, что функциональный компонент должен поддерживать связанный список хуков.Связанный список необходимо создать при первом монтировании, а связанный список необходимо обновлять при последующем обновлении.

Функции ловушек, принадлежащие двум процедурам, будут назначены соответствующим процедурам.ReactCurrentDispatcherна текущем имуществе. Поэтому перед вызовом функционального компонента необходимо определить текущий ReactCurrentDispatcher в соответствии с текущим этапом, чтобы на правильном этапе можно было вызвать правильную функцию ловушки.

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;
}

HooksDispatcherOnMountа такжеHooksDispatcherOnUpdate, функции их внутренних хуков реализованы по-разному, одно из отличий состоит в том, что обработка связанного списка хуков на разных этапах происходит по-разному.

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

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

Знать список крючков

Будь то начальное монтирование или обновление, каждый раз, когда вызывается функция ловушек, будет создан соответствующий ей объект ловушки. Ниже приведена структура объекта-ловушки.

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

Сгенерированные объекты-ловушки упорядочиваются таким образом, чтобы сформировать связанный список и сохранить их в функциональном компоненте fiber.memoizedState. В этом процессе есть очень важный указатель:workInProgressHook, который может косвенно отражать, какая функция ловушки в данный момент вызывается в компоненте, путем записи сгенерированного (обновленного) объекта ловушки. Каждый раз, когда вызывается функция-ловушка, указатель этого указателя перемещается на объект-ловушку, сгенерированный функцией-ловушкой. Например:

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

В приведенном выше примере в компоненте HooksExp вызываются три функции ловушек, а именно useState, useEffect и useState. Тогда процесс построения списка хуков можно резюмировать следующим образом, сосредоточив внимание на изменении workInProgressHook.

Вызовите useState('A'):

fiber.memoizedState: hookA
                       ^
                workInProgressHook

Вызов useEffect:

fiber.memoizedState: hookA -> hookB
                                ^
                         workInProgressHook

Вызовите useState('C'):

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

Каждый раз, когда функция ловушки выполняется, она создает соответствующий объект ловушки для следующего шага.Например, useReducer смонтирует очередь обновлений на объект ловушки, а useEffect смонтирует список эффектов на объект ловушки. Процесс создания объекта ловушки на самом деле является процессом построения списка ловушек и обновления указателя workInProgressHook.

крепление компонента

При первом монтировании на компоненте нет информации о крючках, поэтому этот процесс в основном заключается в создании списка крючков на волокне. Вызов монтированияmountWorkInProgressHook, он создаст хуки и соединит их в связанный список, одновременно обновит workInProgressHook и, наконец, вернет только что созданный хук, который представляет собой связанный список хуков.

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;
}

в настоящее времяRenderingFiber является узлом workInProgress

Мы можем получить объект ловушки, вызвав функцию ловушки в компоненте, например 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];
}

обновление компонента

В процессе обновления, поскольку существует текущее дерево, узел workInProgress также имеет соответствующий текущий узел. Тогда, естественно, будет два связанных списка хуков, которые существуют в свойствах memorizedState текущего узла и узла workInProgress соответственно. Ввиду этого построение списка хуков процесса обновления требует участия еще одного указателя: currentHook. Он служит объектом-ловушкой, соответствующим workInProgressHook компонента при последнем обновлении, и на его основе могут быть созданы новые объекты-ловушки. Кроме того, вы также можете получить некоторые данные последнего объекта ловушки, такие как сравнение до и после зависимостей useEffect, и предыдущие зависимости могут быть получены через него.

                currentTree

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

Следовательно, в дополнение к обновлению указателя workInProgressHook, процесс построения списка ловушек процесса обновления также обновляет указатель currentHook и максимально повторно использует currentHook для создания новых объектов ловушек. Эта процедура вызывает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;
}

Суммировать

Из этой статьи мы узнали, что вызов функции ловушек в функциональном компоненте формирует связанный список ловушек, и этот связанный список будет смонтирован в свойстве memoizedState волокна, соответствующего функциональному компоненту. Это закладывает основу для нашего последующего объяснения функций хуков.