第 27 章

Hooks 链表:useState 和 useEffect 的底层实现

第27章:Hooks 链表:useState 和 useEffect 的底层实现

Hooks 不是魔法,它是对链表数据结构的精心运用——链表的稳定顺序,是函数组件能拥有持久化状态的根本保证。

本章核心问题:'不能在条件语句中调用 Hooks'这条规则背后的实现机制是什么? 读完本章你将理解


Level 1 · 你需要知道的(1-3年经验)

Hook 对象:链表中的一个节点

每个 Hook 调用(useStateuseEffectuseRef 等)在 Fiber 节点上对应链表中的一个节点,节点结构如下:

// packages/react-reconciler/src/ReactFiberHooks.js
// Hook 对象的结构
export type Hook = {
  memoizedState: any,    // 当前稳定状态(对于 useState 是当前值,对于 useEffect 是 effect 对象)
  baseState: any,        // 基础状态(用于计算 update 队列的最终值)
  baseQueue: Update<any, any> | null,  // 基础更新队列(未被处理的低优先级更新)
  queue: UpdateQueue<any, any> | null, // 当前更新队列(环形链表)
  next: Hook | null,     // 指向下一个 Hook 节点
};

这些 Hook 节点以链表形式挂在 Fiber 节点的 memoizedState 字段上。以下面这个组件为例:

function Counter() {
  const [count, setCount] = useState(0);   // Hook 1
  const [name, setName] = useState('');    // Hook 2
  useEffect(() => {                        // Hook 3
    document.title = `Count: ${count}`;
  }, [count]);
  return <div>{count}</div>;
}

渲染时,Fiber 节点的 memoizedState 链表是:

Hook1 (count) → Hook2 (name) → Hook3 (effect) → null

React 通过一个全局指针 currentHookworkInProgressHook 来追踪当前处理到链表的哪个位置。每次调用一个 Hook,指针就前进一步。如果在某次渲染中跳过了一个 Hook(比如条件语句导致某个 Hook 没有执行),链表中的节点就会错位——React 会把第二个节点当成第一个来处理,状态完全乱套。这就是"不能在条件语句中调用 Hooks"规则的根本原因。

双 Dispatcher:挂载与更新的分离

React 使用完全不同的函数实现来处理首次渲染(mount)和后续更新(update):

// packages/react-reconciler/src/ReactFiberHooks.js
const HooksDispatcherOnMount = {
  useState: mountState,
  useEffect: mountEffect,
  useRef: mountRef,
  useMemo: mountMemo,
  useCallback: mountCallback,
  // ...
};

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

// 在 renderWithHooks 中,根据是否首次渲染切换 dispatcher
function renderWithHooks(current, workInProgress, Component, props, ...) {
  // current 为 null 说明是首次渲染
  ReactCurrentDispatcher.current =
    current === null || current.memoizedState === null
      ? HooksDispatcherOnMount
      : HooksDispatcherOnUpdate;
  
  // 调用组件函数,过程中所有 useState 等调用都会路由到当前 dispatcher
  let children = Component(props, secondArg);
  
  // 渲染完成后,切换到报错 dispatcher,防止在渲染外部调用 Hooks
  ReactCurrentDispatcher.current = ContextOnlyDispatcher;
  
  return children;
}

这个设计非常聪明:通过替换 ReactCurrentDispatcher.current 指向的对象,同一个 useState 调用在不同场景下会执行完全不同的代码路径。这是策略模式(Strategy Pattern)的典型应用。

dispatchSetState:触发更新的过程

当你调用 setCount(1) 时,实际执行的是 dispatchSetState

// packages/react-reconciler/src/ReactFiberHooks.js(简化)
function dispatchSetState(fiber, queue, action) {
  const lane = requestUpdateLane(fiber);
  
  const update = {
    lane,
    revertLane: NoLane,
    action,
    hasEagerState: false,
    eagerState: null,
    next: null,
  };
  
  // 优化:如果当前没有其他渲染在进行,尝试提前计算新状态
  // 如果新状态与当前状态相同,可以直接跳过本次渲染(bail out)
  if (isRenderPhaseUpdate(fiber)) {
    // 渲染阶段的更新(组件函数内直接调用 setState)
    didScheduleRenderPhaseUpdateDuringPass = true;
  } else {
    if (fiber.lanes === NoLanes && (alternate === null || alternate.lanes === NoLanes)) {
      // Fiber 上没有其他待处理的工作,尝试 eager state 优化
      const lastRenderedReducer = queue.lastRenderedReducer;
      if (lastRenderedReducer !== null) {
        try {
          const currentState = queue.lastRenderedState;
          const eagerState = lastRenderedReducer(currentState, action);
          update.hasEagerState = true;
          update.eagerState = eagerState;
          if (is(eagerState, currentState)) {
            // 新旧状态相同,不触发重新渲染
            enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update);
            return;
          }
        } catch (error) {
          // 忽略错误,后续正常渲染时会再次计算
        }
      }
    }
    
    // 将 update 加入 queue 的环形链表
    const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
    if (root !== null) {
      scheduleUpdateOnFiber(root, fiber, lane);
    }
  }
}

useEffect 的 Effect 对象

useEffect 的内部实现与 useState 结构类似,但 memoizedState 存储的不是状态值,而是一个 Effect 对象:

// useEffect 的 Effect 对象结构
type Effect = {
  tag: HookFlags,          // 标识 effect 类型(Passive=useEffect, Layout=useLayoutEffect 等)
  create: () => (() => void) | void,  // setup 函数
  inst: {
    destroy: (() => void) | void,     // cleanup 函数(setup 的返回值)
    tag: HookFlags,
  },
  deps: Array<mixed> | null,  // 依赖数组
  next: Effect,               // 指向下一个 Effect(形成环形链表)
};

// packages/react-reconciler/src/ReactFiberHooks.js
function mountEffect(create, deps) {
  return mountEffectImpl(
    PassiveEffect | PassiveStaticEffect,  // Fiber flags
    HookPassive,                           // Hook tag
    create,
    deps,
  );
}

function mountEffectImpl(fiberFlags, hookFlags, create, deps) {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  
  // 标记 Fiber 节点需要处理被动副作用
  currentlyRenderingFiber.flags |= fiberFlags;
  
  hook.memoizedState = pushEffect(
    HookHasEffect | hookFlags,  // 首次挂载:一定要执行 effect
    create,
    createEffectInstance(),
    nextDeps,
  );
}

function updateEffect(create, deps) {
  return updateEffectImpl(PassiveEffect, HookPassive, create, deps);
}

function updateEffectImpl(fiberFlags, hookFlags, create, deps) {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const effect = hook.memoizedState;
  const inst = effect.inst;
  
  if (currentHook !== null) {
    if (nextDeps !== null) {
      const prevDeps = currentHook.memoizedState.deps;
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        // 依赖未变化,不标记 HookHasEffect(不会重新执行)
        hook.memoizedState = pushEffect(hookFlags, create, inst, nextDeps);
        return;
      }
    }
  }
  
  // 依赖变化或无依赖数组,标记需要执行
  currentlyRenderingFiber.flags |= fiberFlags;
  hook.memoizedState = pushEffect(
    HookHasEffect | hookFlags,
    create,
    inst,
    nextDeps,
  );
}

HookHasEffect 是关键标志——只有带有这个标志的 Effect,才会在提交阶段真正执行 create 函数。依赖未变化时,Effect 对象依然存在于链表中(保证链表顺序),但不带 HookHasEffect 标志,所以不会执行。

依赖比较:areHookInputsEqual

// packages/react-reconciler/src/ReactFiberHooks.js
function areHookInputsEqual(nextDeps, prevDeps) {
  if (prevDeps === null) return false;
  
  for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
    if (is(nextDeps[i], prevDeps[i])) {
      continue;
    }
    return false;
  }
  return true;
}

isObject.is 的封装,使用严格相等加上 -0/NaN 的特殊处理。这意味着对象和数组总是被认为是"不同的"(即使内容相同),因为它们是引用比较——这就是为什么 useEffect 的依赖项中放入对象字面量会导致每次渲染都重新执行 effect。

理解了 Hooks 链表的工作原理,你对 React 的理解就从"知道规则"升华到了"理解规则的必然性"。Hooks 不是魔法,它是对链表数据结构的精心运用,而这个链表的稳定顺序,正是 React 函数组件能够拥有持久化状态的根本保证。


Level 2 · 它是怎么运行的(3-5年经验)

useState 的挂载实现:mountState

// packages/react-reconciler/src/ReactFiberHooks.js
function mountState(initialState) {
  // 创建一个新的 Hook 节点,并加入链表末尾
  const hook = mountWorkInProgressHook();
  
  // 支持惰性初始化:initialState 可以是函数
  if (typeof initialState === 'function') {
    initialState = initialState();
  }
  
  hook.memoizedState = hook.baseState = initialState;
  
  // 创建更新队列(环形链表)
  const queue = {
    pending: null,        // 待处理的更新(环形链表的头节点)
    lanes: NoLanes,
    dispatch: null,       // dispatch 函数
    lastRenderedReducer: basicStateReducer,  // useState 使用 basicStateReducer
    lastRenderedState: initialState,
  };
  
  hook.queue = queue;
  
  // 创建并绑定 dispatch 函数
  // dispatch 被绑定到当前 Fiber 和 queue,保证每次调用 setCount 都更新正确的状态
  const dispatch = (queue.dispatch = dispatchSetState.bind(
    null,
    currentlyRenderingFiber,
    queue,
  ));
  
  return [hook.memoizedState, dispatch];
}

// useState 底层使用的 reducer(只是直接返回新值或调用更新函数)
function basicStateReducer(state, action) {
  return typeof action === 'function' ? action(state) : action;
}

useState 的更新实现:updateState

更新时,React 需要处理可能积压的多个 setState 调用:

// packages/react-reconciler/src/ReactFiberHooks.js
function updateState(initialState) {
  // updateReducer 是通用实现,useState 只是用 basicStateReducer 的 useReducer
  return updateReducer(basicStateReducer, initialState);
}

function updateReducer(reducer, initialArg, init) {
  const hook = updateWorkInProgressHook();
  const queue = hook.queue;
  
  queue.lastRenderedReducer = reducer;
  
  const current = currentHook;
  
  // 处理 baseQueue(上次渲染时因优先级不足被跳过的更新)
  let baseQueue = current.baseQueue;
  
  // 合并 pendingQueue 和 baseQueue
  const pendingQueue = queue.pending;
  if (pendingQueue !== null) {
    if (baseQueue !== null) {
      // 将 pendingQueue 合并到 baseQueue 末尾
      const baseFirst = baseQueue.next;
      const pendingFirst = pendingQueue.next;
      baseQueue.next = pendingFirst;
      pendingQueue.next = baseFirst;
    }
    current.baseQueue = baseQueue = pendingQueue;
    queue.pending = null;
  }
  
  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 = removeLanes(update.lane, OffscreenLane);
      const isHiddenUpdate = updateLane !== update.lane;
      
      // 检查当前 update 的优先级是否在本次渲染的 lanes 范围内
      const shouldSkipUpdate = isHiddenUpdate
        ? !isSubsetOfLanes(getWorkInProgressRootRenderLanes(), updateLane)
        : !isSubsetOfLanes(renderLanes, updateLane);
      
      if (shouldSkipUpdate) {
        // 优先级不足,跳过这个 update,保留到 baseQueue 中
        const clone = { ... update };
        if (newBaseQueueLast === null) {
          newBaseQueueFirst = newBaseQueueLast = clone;
          newBaseState = newState;
        } else {
          newBaseQueueLast = newBaseQueueLast.next = clone;
        }
      } else {
        // 优先级满足,应用这个 update
        if (newBaseQueueLast !== null) {
          // 如果前面有跳过的 update,这个也要加入 baseQueue 保证顺序
          newBaseQueueLast = newBaseQueueLast.next = { ...update, lane: NoLane };
        }
        
        // 计算新状态
        newState = reducer(newState, update.action);
      }
      
      update = update.next;
    } while (update !== null && update !== first);
    
    hook.memoizedState = newState;
    hook.baseState = newBaseState === null ? newState : newBaseState;
    hook.baseQueue = newBaseQueueLast;
    
    queue.lastRenderedState = newState;
  }
  
  const dispatch = queue.dispatch;
  return [hook.memoizedState, dispatch];
}

Level 3 · 规范怎么定义的(资深)

本章涉及的 API 规范和设计决策已融入各层级的讨论中。


Level 4 · 边界与陷阱(所有人)

生产环境常见问题

在实际项目中,本章涵盖的概念最常见的问题包括:

  1. 忽视边界条件:在正常路径下工作正常的代码,在异常路径(网络失败、用户快速操作、组件卸载)下可能产生 bug。始终考虑清理和取消逻辑。

  2. 过早优化:在没有测量的情况下添加优化代码,增加了复杂度但不一定提升性能。先用 Profiler 确认问题存在,再实施优化。

  3. 文档与实际行为的差异:React 的行为在不同版本间可能有微妙差异,尤其是并发模式相关的特性。始终以实际测试结果为准。

本章评分
4.8  / 5  (3 评分)

💬 留言讨论