人人都会AI编程

7.3 核心 Hooks 实现原理

更新时间:2026-07-10

理解了 Hooks 的闭包本质和链表存储结构后,我们就能深入剖析 React 中最常用的三个 Hooks——useStateuseEffectuseMemo/useCallback 的底层工作机制。这些原理直接决定了它们在代码中的行为表现,理解它们能让你避开大量隐性 bug。

7.3.1 useState:状态存储与触发更新

useState 是函数组件保持“记忆”的关键。当组件首次渲染时,useState 会在对应的 Hook 节点上初始化一个状态槽,并将初始值存入 hook.memoizedState。在后续渲染中,React 直接从这个槽位读出当前状态。

状态更新并不仅仅是修改内存中的值——它还涉及更新队列调度重渲染

简化版的内部流程

// 伪代码:React 内部对 useState 的抽象实现
function useState(initialValue) {
  const hook = getOrCreateWorkInProgressHook(); // 获取当前 Hook 节点

  // 首次渲染
  if (hook.memoizedState === undefined) {
    hook.memoizedState = typeof initialValue === 'function'
      ? initialValue()
      : initialValue;
  }

  // dispatch 函数,用于更新状态
  const dispatch = (action) => {
    // 计算新状态(支持函数式更新)
    const newState = typeof action === 'function'
      ? action(hook.memoizedState)
      : action;

    // 放入更新队列
    hook.queue.pending = {
      action: newState,  // 简化:实际存储的是 update 对象
      next: null
    };

    // 触发重新渲染
    scheduleUpdate();
  };

  return [hook.memoizedState, dispatch];
}

真正的实现远比这个复杂,主要包含以下细节:

  1. 惰性初始化:如果传给 useState 的是一个函数,React 只会在首次渲染时调用它来获取初始值,后续渲染不再执行该函数。这可以避免昂贵的初始计算。
  1. 更新队列处理:在多次调用 dispatch 时(特别是同一个事件处理函数中连续调用多个 setState),React 不会立即刷新状态,而是将更新对象(update)依次放入队列。在正式进入渲染阶段时,React 会遍历队列,依次应用每个更新(如果是函数,会传入前一个状态),最终计算出最新状态存入 memoizedState
  1. 批量更新(Batching):在 React 18 之前,只有合成事件和生命周期中的 setState 会被批量处理;异步回调(如 setTimeout)中的更新会立即执行。React 18 通过并发模式实现了自动批量更新,任何场景下的多个 setState 都会被合并到一次渲染中。
  1. 闭包陷阱的本质:由于 useState 返回的状态值是每一次渲染快照中的常值(捕获了当前渲染周期的闭包变量),若在异步回调中使用状态的旧值,就会出现“状态滞后”问题。解决方式是使用函数式更新setState(prev => prev + 1)),它不依赖闭包中的旧值,而是基于 React 内部保证的最新状态。

示例:函数式更新避免闭包陷阱

function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const timer = setInterval(() => {
      // ❌ count 始终是首次渲染时的 0
      // setCount(count + 1);

      // ✅ 使用函数式更新,pre 是最新状态
      setCount(prev => prev + 1);
    }, 1000);
    return () => clearInterval(timer);
  }, []); // 空依赖意味着只在挂载时执行一次

  return <h1>{count}</h1>;
}

7.3.2 useEffect:副作用收集与调度

useEffect 的本质是把副作用函数(及其依赖)登记到当前 Fiber 节点的 updateQueue 中,然后由 React 在合适的时机统一执行。

收集阶段(Render 阶段)

在函数组件执行(渲染)期间,调用 useEffect 时会创建一个 effect 对象:

{
  tag: HookFlags.Effect,  // 标记为 effect
  create: () => { /* 副作用函数 */ },
  destroy: undefined,     // 清理函数,在下次 effect 执行前调用
  deps: [dep1, dep2],     // 依赖项数组
  next: null              // 链表指针
}

这个对象被挂载到 hook.memoizedState 上(实际上 useEffect 对应的 Hook 存储的就是一个 effect 链表)。在完成整个组件的渲染后,React 将收集到的所有 effect 放入当前 Fiber 的 updateQueue

执行阶段(Commit 阶段)

真正执行副作用是在 DOM 更新之后(commit 阶段)。React 会根据 effect 的类型(useEffect 是异步非阻塞的,而 useLayoutEffect 是同步的)决定执行时机:

  • useEffect:在浏览器将变更绘制到屏幕之后异步执行,不会阻塞页面视觉更新。
  • useLayoutEffect:在 DOM 变更后浏览器绘制之前同步执行,用于需要同步读取布局信息的场景。

对于每个 effect,React 会先检查依赖项是否变化:

function areDepsEquals(prevDeps, nextDeps) {
  for (let i = 0; i < prevDeps.length; i++) {
    if (Object.is(prevDeps[i], nextDeps[i])) continue;
    return false;
  }
  return true;
}

比较算法是 Object.is,而非浅比较或深比较。这意味:

  • NaNNaN 被视为相等(Object.is(NaN, NaN)true)。
  • 0-0 被视为不相等。
  • 引用类型(对象、数组、函数)只有在引用不变时才算相等。

如果依赖项发生了变化,React 会先调用前一次 effect 的清理函数(destroy),再执行本次的 create 函数;如果没有变化,则完全跳过。

清理函数的执行时机

  • 在依赖变化时,下一次 effect 执行前会先运行上一次的清理函数。
  • 组件卸载时,React 会运行最后一次 effect 的清理函数,避免内存泄漏。
useEffect(() => {
  const subscription = subscribe(props.id);
  return () => {
    // 在 props.id 变化或组件卸载时执行
    subscription.unsubscribe();
  };
}, [props.id]);

7.3.3 useMemo 与 useCallback:值的缓存与函数的稳定引用

这两个 Hook 本质上是同一类优化手段:基于依赖项的缓存。它们都在渲染阶段执行,如果依赖项没有变化,则直接返回上一次的缓存结果

useMemo:缓存计算结果

function useMemo(create, deps) {
  const hook = getOrCreateWorkInProgressHook();

  // 获取上一次的缓存值和依赖项
  const prevState = hook.memoizedState;
  if (prevState !== undefined) {
    const [prevValue, prevDeps] = prevState;
    if (areDepsEquals(prevDeps, deps)) {
      return prevValue; // 依赖未变,返回原值
    }
  }

  // 重新计算并缓存
  const nextValue = create();
  hook.memoizedState = [nextValue, deps];
  return nextValue;
}

useMemo 适合用在计算开销较大且依赖项不频繁变化的场景,如复杂的数据派生。但注意,它本身也有比较依赖项的开销,对于轻量计算可能得不偿失。

useCallback:缓存函数引用

useCallback 的实现与 useMemo 完全一致,只是返回值不同:

function useCallback(callback, deps) {
  return useMemo(() => callback, deps);
}

它的核心价值在于保持函数引用稳定,从而避免子组件不必要的重渲染(当子组件被 React.memo 包裹且依赖函数引用时)。

误区与正确使用

  • 不要无差别地用 useCallback/useMemo 包裹所有函数和值:依赖项比较和缓存本身也有成本,对于轻量计算或几乎总是变化的依赖,缓存起不到优化作用,还增加了代码复杂度。
  • 依赖项必须真实完整:如果在 useCallback 内部使用了某个状态或 props,却未将其加入依赖数组,React 仍会使用旧的闭包值,导致逻辑错误。ESLint 的 exhaustive-deps 规则正是为此而生。

通过理解这三个核心 Hooks 的底层原理,你会发现 React 的 Hooks 体系并非黑魔法,而是一套严谨的闭包 + 链表 + 有序调度机制。掌握这些知识后,像闭包陷阱、无限循环、性能优化失效等问题都将变得透明可控。