Skip to content

第7章 Hooks 的实现原理

本章要点

  • Hooks 的数据结构:单向链表与 Fiber 节点的绑定关系
  • useState 的完整实现:mount 阶段与 update 阶段的差异
  • useReducer 的工作机制:与 useState 的同源性
  • useEffect 与 useLayoutEffect 的内部调度差异
  • useRef 为什么是最简单的 Hook——以及它为什么不触发重渲染
  • useMemo 与 useCallback 的缓存策略与失效机制
  • useContext 的订阅模型与性能陷阱
  • Hook 规则的技术根因:为什么不能条件调用 Hook
  • Dispatcher 切换机制:React 如何在不同阶段使用不同的 Hook 实现

Hooks 是 React 16.8 引入的最重大的范式变革。表面上,它让函数组件拥有了状态和副作用的能力;但从内核的角度看,Hooks 的设计远比 API 表面呈现的要精妙得多。

当你写下 const [count, setCount] = useState(0) 时,你可能会好奇:一个看似无状态的函数,每次调用都从头执行,它是怎么"记住"上一次的值的?更关键的是,当同一个组件中有多个 useState,React 是怎么知道哪个 state 对应哪个调用的?

答案藏在两个核心设计中:Fiber 节点上的 Hook 链表基于调用顺序的索引机制

7.1 Hook 的数据结构

每个 Hook 在 React 内部对应一个 Hook 对象,这些对象通过 next 指针串成一条单向链表,挂载在 Fiber 节点的 memoizedState 属性上:

typescript
// Hook 的核心数据结构
type Hook = {
  memoizedState: any;        // 当前状态值(不同 Hook 存储不同的东西)
  baseState: any;            // 基准状态(用于并发模式下的状态计算)
  baseQueue: Update<any> | null;  // 上次未处理完的更新队列
  queue: UpdateQueue<any> | null; // 当前待处理的更新队列
  next: Hook | null;         // 指向下一个 Hook
};

// 不同 Hook 的 memoizedState 存储的内容
// useState     → state 值
// useReducer   → state 值
// useEffect    → Effect 对象
// useRef       → { current: value }
// useMemo      → [cachedValue, deps]
// useCallback  → [callback, deps]
// useContext   → context 的当前值(但实际上不使用 Hook 链表)

当 React 渲染一个函数组件时,Hook 链表的构建过程如下:

tsx
function MyComponent() {
  const [name, setName] = useState('React');     // Hook 1
  const [count, setCount] = useState(0);          // Hook 2
  const ref = useRef(null);                        // Hook 3
  useEffect(() => { /* ... */ }, [count]);         // Hook 4
  const memoized = useMemo(() => count * 2, [count]); // Hook 5

  return <div>{name}: {count} (x2 = {memoized})</div>;
}

图 7-1:Hook 链表结构示意图

7.2 Dispatcher:Hook 的两张面孔

React 中的每个 Hook API(如 useState)并不是一个固定的实现,而是一个"门面",它的实际行为取决于当前的 Dispatcher。React 维护了一个全局变量 ReactCurrentDispatcher,在不同的执行阶段会切换到不同的 Dispatcher:

typescript
// React 的 Dispatcher 机制
const ReactCurrentDispatcher = {
  current: null as Dispatcher | null,
};

// 三种主要的 Dispatcher
const HooksDispatcherOnMount: Dispatcher = {
  useState: mountState,
  useEffect: mountEffect,
  useRef: mountRef,
  useMemo: mountMemo,
  useCallback: mountCallback,
  useReducer: mountReducer,
  useContext: readContext,
  // ...
};

const HooksDispatcherOnUpdate: Dispatcher = {
  useState: updateState,
  useEffect: updateEffect,
  useRef: updateRef,
  useMemo: updateMemo,
  useCallback: updateCallback,
  useReducer: updateReducer,
  useContext: readContext,
  // ...
};

const InvalidHooksDispatcher: Dispatcher = {
  useState: throwInvalidHookError,
  useEffect: throwInvalidHookError,
  // ... 所有方法都抛错
};

当 React 开始渲染一个函数组件时,会根据情况设置 Dispatcher:

typescript
function renderWithHooks(
  current: Fiber | null,
  workInProgress: Fiber,
  Component: Function,
  props: any
) {
  // 根据是首次渲染还是更新,设置不同的 Dispatcher
  if (current !== null && current.memoizedState !== null) {
    // 更新阶段
    ReactCurrentDispatcher.current = HooksDispatcherOnUpdate;
  } else {
    // 首次挂载
    ReactCurrentDispatcher.current = HooksDispatcherOnMount;
  }

  // 执行函数组件
  let children = Component(props);

  // 渲染完成后,将 Dispatcher 设为无效版本
  // 这就是为什么在组件外调用 Hook 会报错
  ReactCurrentDispatcher.current = InvalidHooksDispatcher;

  return children;
}

这就是为什么在组件渲染函数外部调用 useState 会得到一个"Invalid hook call"错误——此时的 Dispatcher 已经被切换为 InvalidHooksDispatcher,所有 Hook 调用都会抛出异常。

7.3 useState 的完整实现

useState 是使用最广泛的 Hook,它的实现也是理解所有 Hook 工作原理的基石。

7.3.1 Mount 阶段:初始化

首次渲染时,useState 调用的是 mountState

typescript
function mountState<S>(initialState: (() => S) | S): [S, Dispatch<SetStateAction<S>>] {
  // 1. 创建一个新的 Hook 对象并添加到链表末尾
  const hook = mountWorkInProgressHook();

  // 2. 处理初始值(支持函数形式的惰性初始化)
  if (typeof initialState === 'function') {
    initialState = (initialState as () => S)();
  }

  // 3. 设置初始状态
  hook.memoizedState = initialState;
  hook.baseState = initialState;

  // 4. 创建更新队列
  const queue: UpdateQueue<S> = {
    pending: null,
    lanes: NoLanes,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: initialState,
  };
  hook.queue = queue;

  // 5. 创建 dispatch 函数(即 setState)
  const dispatch = (queue.dispatch = dispatchSetState.bind(
    null,
    currentlyRenderingFiber,
    queue
  ));

  return [hook.memoizedState, dispatch];
}

// mountWorkInProgressHook 负责创建 Hook 对象并链接到链表
function mountWorkInProgressHook(): Hook {
  const hook: Hook = {
    memoizedState: null,
    baseState: null,
    baseQueue: null,
    queue: null,
    next: null,
  };

  if (workInProgressHook === null) {
    // 这是链表的第一个 Hook
    currentlyRenderingFiber.memoizedState = hook;
    workInProgressHook = hook;
  } else {
    // 追加到链表末尾
    workInProgressHook.next = hook;
    workInProgressHook = hook;
  }

  return hook;
}

有一个重要的细节:dispatch 函数通过 bind 绑定了 Fiber 节点和更新队列。这就是为什么 setState 可以在组件外部被调用(比如在 setTimeout 中)——它已经"记住"了要更新哪个组件。

7.3.2 Update 阶段:处理更新

当组件因为 setState 被调用而重新渲染时,useState 调用的是 updateState

typescript
function updateState<S>(initialState: (() => S) | S): [S, Dispatch<SetStateAction<S>>] {
  // useState 在 update 阶段本质上就是一个 useReducer
  return updateReducer(basicStateReducer, initialState);
}

// useState 的 reducer 就是这么简单
function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
  return typeof action === 'function'
    ? (action as (S) => S)(state)
    : action;
}

这揭示了一个重要事实:useState 在内部就是一个使用了 basicStateReduceruseReducer

typescript
function updateReducer<S, A>(
  reducer: (S, A) => S,
  initialArg: S
): [S, Dispatch<A>] {
  // 1. 获取当前 Hook(从链表中按顺序取下一个)
  const hook = updateWorkInProgressHook();
  const queue = hook.queue!;

  queue.lastRenderedReducer = reducer;

  const current = currentHook!;
  let baseQueue = current.baseQueue;

  // 2. 将 pending 更新合并到 baseQueue
  const pendingQueue = queue.pending;
  if (pendingQueue !== null) {
    if (baseQueue !== null) {
      // 合并两个环形链表
      const baseFirst = baseQueue.next;
      const pendingFirst = pendingQueue.next;
      baseQueue.next = pendingFirst;
      pendingQueue.next = baseFirst;
    }
    current.baseQueue = baseQueue = pendingQueue;
    queue.pending = null;
  }

  // 3. 逐个处理更新队列中的 update
  if (baseQueue !== null) {
    const first = baseQueue.next;
    let newState = current.baseState;
    let newBaseState = null;
    let newBaseQueueFirst = null;
    let newBaseQueueLast = null;
    let update = first;

    do {
      const updateLane = update.lane;

      if (!isSubsetOfLanes(renderLanes, updateLane)) {
        // 优先级不够,跳过此更新(保留到下次)
        const clone = {
          lane: updateLane,
          action: update.action,
          hasEagerState: update.hasEagerState,
          eagerState: update.eagerState,
          next: null as any,
        };
        if (newBaseQueueLast === null) {
          newBaseQueueFirst = newBaseQueueLast = clone;
          newBaseState = newState;
        } else {
          newBaseQueueLast = newBaseQueueLast.next = clone;
        }
      } else {
        // 优先级足够,计算新状态
        if (update.hasEagerState) {
          // 使用预计算的状态(性能优化)
          newState = update.eagerState;
        } else {
          const action = update.action;
          newState = reducer(newState, action);
        }
      }

      update = update.next;
    } while (update !== null && update !== first);

    hook.memoizedState = newState;
    hook.baseState = newBaseState ?? newState;
    hook.baseQueue = newBaseQueueLast;

    queue.lastRenderedState = newState;
  }

  return [hook.memoizedState, queue.dispatch!];
}

7.3.3 dispatchSetState:更新的触发

当你调用 setCount(count + 1) 时,真正执行的是 dispatchSetState

typescript
function dispatchSetState<S, A>(
  fiber: Fiber,
  queue: UpdateQueue<S, A>,
  action: A
) {
  // 1. 获取更新优先级
  const lane = requestUpdateLane(fiber);

  // 2. 创建 update 对象
  const update: Update<S, A> = {
    lane,
    action,
    hasEagerState: false,
    eagerState: null,
    next: null as any,
  };

  if (isRenderPhaseUpdate(fiber)) {
    // 在渲染过程中调用 setState(render phase update)
    enqueueRenderPhaseUpdate(queue, update);
  } else {
    // 3. 🔑 关键优化:Eager State
    const alternate = fiber.alternate;
    if (
      fiber.lanes === NoLanes &&
      (alternate === null || alternate.lanes === NoLanes)
    ) {
      // 当前没有其他待处理的更新
      // 可以立即计算新状态,如果和旧状态相同就跳过调度
      const lastRenderedReducer = queue.lastRenderedReducer;
      if (lastRenderedReducer !== null) {
        const currentState = queue.lastRenderedState;
        const eagerState = lastRenderedReducer(currentState, action);
        update.hasEagerState = true;
        update.eagerState = eagerState;

        if (Object.is(eagerState, currentState)) {
          // 新旧状态相同,无需调度更新!
          enqueueUpdate(fiber, queue, update, lane);
          return;
        }
      }
    }

    // 4. 将 update 加入队列
    enqueueUpdate(fiber, queue, update, lane);

    // 5. 调度更新
    const root = scheduleUpdateOnFiber(fiber, lane);
  }
}

基于 VitePress 构建