Skip to content

整体思路

React Hooks 是 React 16.8 版本中引入的新特性,它允许我们在函数式组件中使用 state 和其他 React 特性。从而为我们提供了全新的组件实现方式,将组件状态的管理 和 生命周期的维护可以在 函数中实现。

下面我们带着以下几个问题去了解 React 中 Hooks 的整体流程。

  1. 怎么在函数中维护组件的状态的,而不会因为函数的多次执行而导致状态的丢失?即为什么 useState 等方法,在函数式组件初次渲染的时候会生成新的值,但是在更新的时候,值不会改变?
  2. 怎么在函数中维护组件的生命周期的?
  3. 函数中 hooks 的状态是怎么维护,其结构是什么?
  4. 函数中 hooks 的更新是异步的,那么怎么保证状态的更新是按照顺序的?如 setNum(num + 'A');setNum(num + 'B'); 这个是 AB 而不会变成 BA
  5. 为什么 React 中要求我们所有的 Hooks 不能在循环、条件判断或者嵌套函数中调用?

下面我们先看一个例子

栗子

js
const App = (props) => {
  // 添加一个 hook
  const [visible, setVisible] = React.useState(true);
  // 添加一个 hook
  const [num, setNum] = React.useState(1);

  const handleClickBtn = () => {
    // setVisible hook上添加一个 update
    setVisible(!visible);
  };
  // setNum hook上添加一个 update
  setNum(num + 1);
  // setNum hook上添加一个 update
  setNum(num + 1);

  return (
    <div>
      <button onClick={handleClickBtn}>切换</button>
    </div>
  );
};

这个组件中存在两个 hooks,一个是 useState(visiable),一个是 useState(1), 其在组件 FiberNode 中存储的结构如下图所示,通过一个循环链表的方式将组件中所有的 hooks 串联起来,形成一个单向链表的结构,并保存到 FiberNode.memoizedState 上。

这样在组件更新的时候,我们就可以按照顺序去从 memoizedState 链表中获取到对应顺序的 hook , 然后根据 hook 上的 update 队列,计算出最新的 state 值。

所以为什么 React 中要求我们所有的 Hooks 不能在循环、条件判断或者嵌套函数中调用,因为在循环、条件判断等条件中,每一次执行的 hooks 顺序是不确定的,导致 hooks 的状态无法维护。

image

‍ 下面我们带着以下几个问题去具体的了解 React 中 Hooks 的实现

  1. 为什么 useState 等在 setState 后,新值需要在组件的下一次渲染的时候才会生效?

类型

  • useCallback |
  • useContext
  • useEffect
  • useImperativeHandle
  • useLayoutEffect
  • useInsertionEffect | HookInsertion
  • useMemo
  • useReducer
  • useRef
  • useState
  • useDebugValue
  • useDeferredValue
  • useTransition
  • useMutableSource
  • useSyncExternalStore
  • useId

源码解读

下面我们通过 useState 去了解 Hooks 中整体实现思路

js
export function useState<S>(
  initialState: (() => S) | S
): [S, Dispatch<BasicStateAction<S>>] {
  const dispatcher = resolveDispatcher();
  return dispatcher.useState(initialState);
}
/**
 * 加载全局变量对象 ReactCurrentDispatcher.current 中存储的 useState 方法
 */
function resolveDispatcher() {
  const dispatcher = ReactCurrentDispatcher.current;
  return ((dispatcher: any): Dispatcher);
}

从上面我们可以看出,useState 方法是从 ReactCurrentDispatcher.current 中获取的,那么这个变量是从哪来的呢?我们是不是可以在函数式组件执行之前去根据组件 current === null 来判断是初次渲染还是更新渲染,然后将不同的方法赋值给 ReactCurrentDispatcher.current 呢?这样就可以解决上面 问题 1 , 因为函数式组件在初次渲染和更新的时候, useState 虽然是同一个方法,但是其具体实现已经不同了。具体如下:

hooks 的如何在不同阶段使用不同的 hooks 方法

js
/**
 * 执行函数式组件,并返回组件的渲染结果。
 *   1. 通过 current的判断去决定执行 mount 还是 update 阶段的 hooks 方法,并赋值给 ReactCurrentDispatcher.current
 *   2. 执行函数式组件,获取组件的渲染结果(对于组件中的 hooks 这时候就可以直接使用 ReactCurrentDispatcher.current)
 *   3. 处理函数式组件的 执行阶段出发的 更新,并判断是否超过25次的渲染次数,以防止无限循环
 * @returns
 */
export function renderWithHooks<Props, SecondArg>(
  current: Fiber | null,
  workInProgress: Fiber,
  Component: (p: Props, arg: SecondArg) => any,
  props: Props,
  secondArg: SecondArg,
  nextRenderLanes: Lanes
): any {
  // 通过全局变量的方式缓存当前Hooks方法函数
  //  -- 初次渲染的时候 使用的是hook的 mountState 等方法
  //  -- 更新   的时候 使用的是hooks的 updateState 等方法
  ReactCurrentDispatcher.current =
    current === null || current.memoizedState === null
      ? HooksDispatcherOnMount
      : HooksDispatcherOnUpdate;
  // 执行函数式组件
  let children = Component(props, secondArg);
  // 检查组件渲染期间可能发生的状态更新。
  // 如果为 true ,则表示在组件的渲染阶段发生了状态更新(setState),需要重新渲染组件。
  // 如在函数组件体内部直接调用了setState。这种情况会导致React需要重新渲染组件,
  // 以确保状态更新被正确处理。为了防止无限循环,React会限制重新渲染的次数,
  // 最多25次,超过就会抛出错误
  if (didScheduleRenderPhaseUpdateDuringThisPass) {
    // Keep rendering in a loop for as long as render phase updates continue to
    // be scheduled. Use a counter to prevent infinite loops.
    let numberOfReRenders: number = 0;
    do {
      didScheduleRenderPhaseUpdateDuringThisPass = false;
      localIdCounter = 0;

      // 修改 react hooks的实现 指向渲染阶段更新的hooks方法
      //  -- 因为在函数式组件体内部直接调用了setState。这种情况会导致React需要重新渲染组件,
      //  -- 以确保状态更新被正确处理。为了防止无限循环,React会限制重新渲染的次数,最多25次,超过就会抛出错误
      //  -- 所以需要重新指向渲染阶段更新的hooks方法
      ReactCurrentDispatcher.current = __DEV__
        ? HooksDispatcherOnRerenderInDEV
        : HooksDispatcherOnRerender;

      children = Component(props, secondArg);
    } while (didScheduleRenderPhaseUpdateDuringThisPass);
  }
  // 重置 hooks 函数 ,防止其他地方调用 hooks 方法,被污染
  ReactCurrentDispatcher.current = ContextOnlyDispatcher;

  return children;
}

在 beginWork 阶段,如果是函数式组件,会执行函数生成新的 FiberNode 节点,这时候就可以根据 current === null || current.memoizedState === null去判断是初次渲染还是更新渲染,然后将 初次渲染阶段的 HooksDispatcherOnMount 或 更新阶段的 HooksDispatcherOnUpdate 赋值给 ReactCurrentDispatcher.current。同时如果在更新阶段又触发了组件的重新更新操作(组件内部直接调用 setState 等),那么为了保证 hooks 顺序和链表正确,所以需要将 HooksDispatcherOnRerender 赋值给 ReactCurrentDispatcher.current, 这样对于 React Hooks,其每一个 hook 最多可以包含 3 种类型的实现方法,分别是

1. HooksDispatcherOnMount(首次挂载 )

tsx
const HooksDispatcherOnMount: Dispatcher = {
  readContext,
  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,
  useMutableSource: mountMutableSource,
  useSyncExternalStore: mountSyncExternalStore,
  useId: mountId,
  unstable_isNewReconciler: enableNewReconciler,
};

作用阶段 :组件首次挂载(mount)时使用。

特点 :mount 版本的 hooks 负责初始化 hooks 链表、分配初始 state、注册 effect 等。

对于 useState来说,其首次挂载阶段使用的 hook 方法是 mountState,其具体实现如下:

jsx
function mountState<S>(
  initialState: (() => S) | S
): [S, Dispatch<BasicStateAction<S>>] {
  // 获取或者创建一个 hook 对象
  const hook = mountWorkInProgressHook();
  // 初始化执行  useState(xxx)的入参,如果是函数,执行函数获取初始值
  if (typeof initialState === "function") {
    // $FlowFixMe: Flow doesn't like mixed types
    initialState = initialState();
  }
  // 将初始值赋值给 hook 对象的 memoizedState ,baseState 属性
  hook.memoizedState = hook.baseState = initialState;
  // 创建一个队列,用于存储更新
  const queue: UpdateQueue<S, BasicStateAction<S>> = {
    pending: null,
    lanes: NoLanes,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: (initialState: any),
  };
  // 将队列赋值给 hook 对象的 queue 属性
  hook.queue = queue;
  // 创建一个 dispatch 方法,用于更新状态
  const dispatch: Dispatch<BasicStateAction<S>> = (queue.dispatch =
    (dispatchSetState.bind(null, currentlyRenderingFiber, queue): any));
  return [hook.memoizedState, dispatch];
}

其与其他阶段的核心区别之一就是 : 对于当前 hook 对象的获取,其使用的也是有 mount 标记的获取方法,即const hook = mountWorkInProgressHook(),其具体实现看下面专门讲解,这边简单说一下其主要作用就是:

初始化一个 hook 对象,并将其添加到 workInProgressHook 链表中

  • hook 存储的为一个 Hook 对象类型
  • 一个函数组件中所有的 hook 是以链表的形式存储在 fiberNode.memoizedState 中的

2. HooksDispatcherOnUpdate

tsx
const HooksDispatcherOnUpdate: Dispatcher = {
  readContext,

  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,
  useMutableSource: updateMutableSource,
  useSyncExternalStore: updateSyncExternalStore,
  useId: updateId,

  unstable_isNewReconciler: enableNewReconciler,
};
  • 作用阶段 :组件更新(update)时使用。
  • 暴露方法 :useState、useEffect 等都指向“update”版本(如 updateState、updateEffect)。
  • 特点 :update 版本的 hooks 负责读取和更新 hooks 链表中的已有 state、判断依赖是否变化、决定是否执行 effect 等。

还是看 useState来说,其更新阶段使用的 hook 方法是 updateState,其具体实现如下:

3. HooksDispatcherOnRerender

tsx
const HooksDispatcherOnRerender: Dispatcher = {
  readContext,

  useCallback: updateCallback,
  useContext: readContext,
  useEffect: updateEffect,
  useImperativeHandle: updateImperativeHandle,
  useInsertionEffect: updateInsertionEffect,
  useLayoutEffect: updateLayoutEffect,
  useMemo: updateMemo,
  useReducer: rerenderReducer,
  useRef: updateRef,
  useState: rerenderState,
  useDebugValue: updateDebugValue,
  useDeferredValue: rerenderDeferredValue,
  useTransition: rerenderTransition,
  useMutableSource: updateMutableSource,
  useSyncExternalStore: updateSyncExternalStore,
  useId: updateId,

  unstable_isNewReconciler: enableNewReconciler,
};
  • 作用阶段 :在组件渲染过程中,如果发生了“渲染阶段更新”(如在 render 期间调用 setState),React 会重新渲染组件,此时会使用 rerender 版本的 dispatcher。
  • 暴露方法 :useState、useEffect 等都指向“rerender”版本(如 rerenderState、rerenderEffect)。
  • 特点 :rerender 版本的 hooks 主要用于特殊情况,确保在 render 阶段多次调用 hooks 时,hooks 链表能正确维护,防止顺序错乱。

4. ContextOnlyDispatcher

tsx
export const ContextOnlyDispatcher: Dispatcher = {
  readContext,

  useCallback: throwInvalidHookError,
  useContext: throwInvalidHookError,
  useEffect: throwInvalidHookError,
  useImperativeHandle: throwInvalidHookError,
  useInsertionEffect: throwInvalidHookError,
  useLayoutEffect: throwInvalidHookError,
  useMemo: throwInvalidHookError,
  useReducer: throwInvalidHookError,
  useRef: throwInvalidHookError,
  useState: throwInvalidHookError,
  useDebugValue: throwInvalidHookError,
  useDeferredValue: throwInvalidHookError,
  useTransition: throwInvalidHookError,
  useMutableSource: throwInvalidHookError,
  useSyncExternalStore: throwInvalidHookError,
  useId: throwInvalidHookError,

  unstable_isNewReconciler: enableNewReconciler,
};
  • 作用阶段 :在非函数式组件中调用 hook 方法。
  • 暴露方法 :throwInvalidHookError。
  • 特点 :在非函数式组件中调用 hook 方法,进行错误的暴露。

我们看一下这两个对象

Mount阶段image-20250508141248402

依赖的变量

需要注意的是 Hooks 的渲染流程是在 beginWork 中

workInProgressHook

跟 workInProgress 一样,是一个指向当前组件正在执行的 hook 对象的属性

image

  1. 初次渲染阶段

通过 mountWorkInProgressHook创建一个 hook 对象,并将当前 hook 对象赋值给 workInProgressHook, 然后将 workInProgressHook 赋给当前组件的 FiberNode.memoizedState 或者 workInProgressHook.next , 从而将当前 hook 添加到 FiberNode.memoizedState 链表中

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

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

    next: null,
  };
  // 如果当前函数组件第一次初始化 Hooks
  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;
}
  1. 更新阶段

通过 updateWorkInProgressHook 函数去获取 FiberNode.memoizedState上的当前顺序的 hook 对象

FiberNode.memoizedState

这是一个链表结构的对象,缓存了当前函数式组件中所有的 hooks。 image

hooks 对象的保存和获取

上面例子中,在 App 组件初始渲染的时候,我们知道 state 的值为 useState(1)的初始化值 1,那么在 effect 执行后更新到 2、3 的时候,这个时候函数式组件又会从上到下执行,那么这时候为什么 React 知道 num 的值是 3 而不是 初始化值 1 呐?

这就涉及到 useState 的值是如何保存和读取的,如果在初次渲染的时候使用初始化值,在更新渲染的时候使用新的状态值。

从上面 hooks 在不同的阶段使用不同的 hook 实现方法中知道,hook 的执行可以通过一定的方法获取到其执行环境,如

  • 初次渲染阶段 mountXXXX
  • 更新渲染阶段 updateXXXX
  • 渲染阶段更新阶段 rerenderXXXX

那么我们通过源码也就可以发现在其不同的阶段,其获取 hook 的方法也是不同的

初次渲染阶段

在初次渲染阶段,其获取 hook 的方法是 mountWorkInProgressHook,其具体实现如下:

mountWorkInProgressHook

js
function mountWorkInProgressHook(): Hook {
  const hook: Hook = {
    // 用于保存一次更新渲染流程的初始值 --- 对应的是 render的值
    memoizedState: null,
    // 用于保存一次渲染流程中多个update的值
    baseState: null,

    baseQueue: null,
    // 保存了 update 更新链
    queue: null,
    // 指向下一个hook
    next: null,
  };

  // 如果当前函数组件第一次初始化 Hooks
  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;
}

其主要是创建一个通用的 hook 对象,然后将其添加到 FiberNode.memoizedState 链表中。

更新渲染、渲染阶段更新阶段

updateWorkInProgressHook()

js
/**
 * 函数式组件
 *  组件更新渲染的时候,执行到 useState等Hook 按照新FiberNode.alternate(久的FiberNode)的链表去在FiberNode.memoizedState上复制hook链;
 * @returns
 */
function updateWorkInProgressHook(): Hook {
  let nextCurrentHook: null | Hook;
  // currentHook为上一个Hook
  // 1. 如果 currentHook === null 说明这是处理的第一个 hook 那就通过 FiberNode.alternate.memoizedState 获取旧的FiberNode的第一个hook
  // 2. 如果 currentHook !== null 说明这是第二个及后面的 这时候就通过 currentHook nextCurrentHook 获取到当前hook在初次渲染的时候创建的 hook对象内容
  if (currentHook === null) {
    // 获取FiberNode 的 原来的FiberNode
    const current = currentlyRenderingFiber.alternate;
    // 有久的FiberNode
    if (current !== null) {
      // 那就将 nextCurrentHook 指向第一个 hook
      nextCurrentHook = current.memoizedState;
    } else {
      nextCurrentHook = null;
    }
  } else {
    // 将上一个 Hook.next 作为当前的 hook
    nextCurrentHook = currentHook.next;
  }

  // 跟 currentHook、nextCurrentHook的逻辑一样
  // 通过 workInProgressHook 、nextWorkInProgressHook
  // 在新的FiberNode.memoizedState上按照 nextCurrentHook的数据生成新的hook单向链表结构
  // 注意:
  // 1. nextWorkInProgressHook 为 null
  // 生成新的hook(newHook)的时候,其newHook.next === null
  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.
    // 将next作为当前处理的 hook
    currentHook = nextCurrentHook;
    // 生成一个新的 hook
    const newHook: Hook = {
      memoizedState: currentHook.memoizedState,

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

      next: null,
    };
    // 将新的hook单向链表添加到新的FiberNode.memoizedState上
    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;
}

通过FiberNode.memoizedState中按照初始化的时候一个个添加的顺序,从中获取到当前顺序对应的 hook 对象,所以需要确保 hooks 的顺序和数量与上一次渲染一致,否则会导致状态不一致的问题。

其仍然继承 React 中双 FiberNode 树的概念,通过 currentHooknextCurrentHook 来获取 currentlyRenderingFiber.alternate旧节点树上保存的 hook 对象,然后在生产环境下对于更新阶段 最后的条件语句产生的 hook 数量不一致的情况,其生成或者复用老的 hook 对象,从而保证 hooks 的数量和顺序一致。(不要在 hook 中间去产生条件判断,这样顺序就不对了

image

上面我们知道 每一个 useStateuseReduceruseEffect 等都会在 FiberNode.memoizedState 上添加对应的 hook 对象, 那么这个 hook 对象的结构是什么样的呐?每一个属性作用是什么呐?

hook 对象结构

memoizedState

用于保存一次更新渲染流程的初始值 --- 对应的是 render 的值

在 render() 阶段 ,我们访问的值就是 memoizedState 上的值

baseState

用于保存因为优先级不同导致 Update 跳过之前计算的最终 state 的值(newState),如下述栗子的 "A",

从而为下次计算不会因为之前的 update 不再执行,从而导致 state 的值丢失 (ABCD 不会变成 BCD)

baseQueue

保存因为优先级不同导致 Update 不执行的第一个跳过的链表,在下次执行的时候作为 baseQueue 的头部

queue

保存了此次触发中产生的 Update,用于在组件 render 的时候根据 update 的链表计算出最终的值

next

指向下一个 hook, 从而形成一个 hook 的单向链表结构