原文

1. 背景

有 React Hook 使用经验的小伙伴应该知道,如果使用 hooks 之时用了条件语句,那么编辑器通常会有报错:

image.png
会报错是一样,React Hooks 必须要确保在每一次渲染的时候执行顺序都是一样的。但是,为什么有这么「奇怪」的规定呢?

2. React Hooks 源码解读

2.1 renderWithHooks

顾名思义,这个函数会在渲染阶段执行。为了避免干扰,我们可以在这个阶段先忽略 renderHooks 前面的动作。

首先,这个函数会有一段初始化的操作:

renderLanes = nextRenderLanes;
currentlyRenderingFiber = workInProgress;

if (__DEV__) {
  hookTypesDev =
    current !== null
      ? ((current._debugHookTypes: any): Array<HookType>)
      : null;
  hookTypesUpdateIndexDev = -1;
  // Used for hot reloading:
  ignorePreviousDependencies =
    current !== null && current.type !== workInProgress.type;

  warnIfAsyncClientComponent(Component);
}

workInProgress.memoizedState = null;
workInProgress.updateQueue = null;
workInProgress.lanes = NoLanes;

接着,会根据是否为 dev 环境,来取对应的 dispatcher

if (__DEV__) {
  if (current !== null && current.memoizedState !== null) {
    ReactSharedInternals.H = HooksDispatcherOnUpdateInDEV;
  } else if (hookTypesDev !== null) {
    // This dispatcher handles an edge case where a component is updating,
    // but no stateful hooks have been used.
    // We want to match the production code behavior (which will use HooksDispatcherOnMount),
    // but with the extra DEV validation to ensure hooks ordering hasn't changed.
    // This dispatcher does that.
    ReactSharedInternals.H = HooksDispatcherOnMountWithHookTypesInDEV;
  } else {
    ReactSharedInternals.H = HooksDispatcherOnMountInDEV;
  }
} else {
  ReactSharedInternals.H =
    current === null || current.memoizedState === null
      ? HooksDispatcherOnMount
      : HooksDispatcherOnUpdate;
}

以非 DEV 环境为例,我们注意到,如果 current === null || current.memoizedState === null 的话,取的就是 HooksDispatcherOnMount,反之取的就是 HooksDispatcherOnUpdate
因此,顾名思义,mount 阶段和 update 阶段的 hooks 是分开的。这个从实际的代码定义中也可以得到佐证

<details>

<summary>HooksDispatcherOnMount</summary>

const HooksDispatcherOnMount: Dispatcher = {
  readContext,

  use,
  useCallback: mountCallback,
  useContext: readContext,
  useEffect: mountEffect,
  useImperativeHandle: mountImperativeHandle,
  useLayoutEffect: mountLayoutEffect,
  useInsertionEffect: mountInsertionEffect,
  useMemo: mountMemo,
  useReducer: mountReducer,
  useRef: mountRef,
  useState: mountState,
  useDebugValue: mountDebugValue,
  useDeferredValue: mountDeferredValue,
  useTransition: mountTransition,
  useSyncExternalStore: mountSyncExternalStore,
  useId: mountId,
  useHostTransitionStatus: useHostTransitionStatus,
  useFormState: mountActionState,
  useActionState: mountActionState,
  useOptimistic: mountOptimistic,
  useMemoCache,
  useCacheRefresh: mountRefresh,
};

</details>

<details>

<summary>HooksDispatcherOnUpdate</summary>

const HooksDispatcherOnUpdate: Dispatcher = {
  readContext,

  use,
  useCallback: updateCallback,
  useContext: readContext,
  useEffect: updateEffect,
  useImperativeHandle: updateImperativeHandle,
  useInsertionEffect: updateInsertionEffect,
  useLayoutEffect: updateLayoutEffect,
  useMemo: updateMemo,
  useReducer: updateReducer,
  useRef: updateRef,
  useState: updateState,
  useDebugValue: updateDebugValue,
  useDeferredValue: updateDeferredValue,
  useTransition: updateTransition,
  useSyncExternalStore: updateSyncExternalStore,
  useId: updateId,
  useHostTransitionStatus: useHostTransitionStatus,
  useFormState: updateActionState,
  useActionState: updateActionState,
  useOptimistic: updateOptimistic,
  useMemoCache,
  useCacheRefresh: updateRefresh,
};

</details>

2.2 mountState

mount 阶段的 useState 会执行 mountState:

function mountState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  const hook = mountStateImpl(initialState);
  const queue = hook.queue;
  const dispatch: Dispatch<BasicStateAction<S>> = (dispatchSetState.bind(
    null,
    currentlyRenderingFiber,
    queue,
  ): any);
  queue.dispatch = dispatch;
  return [hook.memoizedState, dispatch];
}

2.2.1 mountWorkInProgressHook

这个函数会做一些初始化,主要功能如下:

  1. 如果 workInProgressHook 为空,那么就创建链表头部,并且赋值给 currentlyRenderingFiber
  2. 如果 workInProgressHook 不为空,那么就取下一个链表节点的值作为 workInProgressHook 的值
function mountWorkInProgressHook(): Hook {
  const hook: Hook = {
    memoizedState: null,

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

    next: null,
  };

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

2.2.2 mountStateImpl

这个函数会把用户传入的 state 赋值给 hook.baseStatehook.memoizedState
另外,还初始化了 hook.queue。\
目前还不知道 hook.queue 有什么作用,所以先暂时不管它。

function mountStateImpl<S>(initialState: (() => S) | S): Hook {
  const hook = mountWorkInProgressHook();
  if (typeof initialState === 'function') {
    const initialStateInitializer = initialState;
    // $FlowFixMe[incompatible-use]: Flow doesn't like mixed types
    initialState = initialStateInitializer();
    if (shouldDoubleInvokeUserFnsInHooksDEV) {
      setIsStrictModeForDevtools(true);
      try {
        // $FlowFixMe[incompatible-use]: Flow doesn't like mixed types
        initialStateInitializer();
      } finally {
        setIsStrictModeForDevtools(false);
      }
    }
  }
  hook.memoizedState = hook.baseState = initialState;
  const queue: UpdateQueue<S, BasicStateAction<S>> = {
    pending: null,
    lanes: NoLanes,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: (initialState: any),
  };
  hook.queue = queue;
  return hook;
}

2.3 updateState

updateState 会在页面 update 的阶段执行,这里的代码比较「简单」,因为内部也是直接拿的 useReducer 来执行的:

function updateState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  return updateReducer(basicStateReducer, initialState);
}

2.3.1 updateReducer

updateReducer 分为两个部分:

  • updateWorkInProgressHook
  • updateReducerImpl
function updateReducer<S, I, A>(
  reducer: (S, A) => S,
  initialArg: I,
  init?: I => S,
): [S, Dispatch<A>] {
  const hook = updateWorkInProgressHook();
  return updateReducerImpl(hook, ((currentHook: any): Hook), reducer);
}
2.3.1.1 updateWorkInProgressHook

首先,开头的注释就已经解释了这部分代码的用途:

// This function is used both for updates and for re-renders triggered by a\
// render phase update. It assumes there is either a current hook we can\
// clone, or a work-in-progress hook from a previous render pass that we can\
// use as a base.

函数一开始就会去拿 nextCurrentHook。正如前面所看到的,这里其实是用链表来维护的,所以这里也是链表相关的操作:

let nextCurrentHook: null | Hook;
if (currentHook === null) {
  const current = currentlyRenderingFiber.alternate;
  if (current !== null) {
    nextCurrentHook = current.memoizedState;
  } else {
    nextCurrentHook = null;
  }
} else {
  nextCurrentHook = currentHook.next;
}

接着,代码里会通过 workInProgressHook 来获取 nextWorkInProgressHook

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

最后,workInProgressHookcurrentHook 都会被更新:

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.

  if (nextCurrentHook === null) {
    const currentFiber = currentlyRenderingFiber.alternate;
    if (currentFiber === null) {
      // This is the initial render. This branch is reached when the component
      // suspends, resumes, then renders an additional hook.
      // Should never be reached because we should switch to the mount dispatcher first.
      throw new Error(
        'Update hook called on initial render. This is likely a bug in React. Please file an issue.',
      );
    } else {
      // This is an update. We should always have a current hook.
      throw new Error('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;
  }
}

<details>
<summary>完整源码</summary>

function updateWorkInProgressHook(): Hook {
  // This function is used both for updates and for re-renders triggered by a
  // render phase update. It assumes there is either a current hook we can
  // clone, or a work-in-progress hook from a previous render pass that we can
  // use as a base.
  let nextCurrentHook: null | Hook;
  if (currentHook === null) {
    const current = currentlyRenderingFiber.alternate;
    if (current !== null) {
      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.

    if (nextCurrentHook === null) {
      const currentFiber = currentlyRenderingFiber.alternate;
      if (currentFiber === null) {
        // This is the initial render. This branch is reached when the component
        // suspends, resumes, then renders an additional hook.
        // Should never be reached because we should switch to the mount dispatcher first.
        throw new Error(
          'Update hook called on initial render. This is likely a bug in React. Please file an issue.',
        );
      } else {
        // This is an update. We should always have a current hook.
        throw new Error('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;
}

</details>

其实,看到这里已经基本了解了 hook 大概的运行情况了。下面的代码逻辑太复杂了,可以留到下次再分析~

3. 总结

3.1 React.useState 的运转逻辑

mount 阶段:

  1. 执行 mountState,内部在初始化阶段执行 mountWorkInProgressHook
  2. mountWorkInProgressHook 最终会生成一条链表来存储 state 和对应的 dispatcher
  3. state 生成的链表,链表头节点指向的是 workInProgressHook,并且它也是一个全局变量

update 阶段:

  1. 执行 useReducer
  2. 执行 updateWorkInProgressHook,并且更新 currentHookworkInProgressHook,这两个都是全局变量
  3. 执行 updateReducerImpl 并更新 state

3.2 为什么 hooks 使用时不能用条件语句

最后让我们回到一开始的问题:为什么 React Hooks 不能用条件语句来执行?

看完上面整个分析的流程,其实已经能够理解了,以上面的 useState 为例:

  1. state 是存储在链表的某个节点上的,不同的 state 都在一条链表上,因此它们之间是有顺序关系的。
  2. 更新阶段,是通过遍历链表来获取 state 和对应的 dispatcher。因此如果出现条件语句,那么很有可能会出现取错 statedispatcher 的情况,从而导致异常。

tonychen
1.2k 声望272 粉丝