前置知识: React

Hooks 原理

4 min高级

React Hooks底层实现原理

前置知识

学习目标

  • 掌握「概述」的核心机制、典型用法与常见陷阱
  • 掌握「基础概念」的核心机制、典型用法与常见陷阱
  • 掌握「快速上手」的核心机制、典型用法与常见陷阱
  • 掌握「详细用法」的核心机制、典型用法与常见陷阱
  • 掌握「常见场景」的核心机制、典型用法与常见陷阱

概述

React Hooks 的底层实现基于 Fiber 架构。每个函数组件对应的 Fiber 节点上挂载了一个 Hooks 链表,Hooks 按调用顺序以链表形式串联。理解 Hooks 的底层原理有助于避免常见的使用错误(如条件调用 Hooks),也能帮助开发者编写更高效的自定义 Hooks。

基础概念

Hooks 链表结构

Hooks 在 Fiber 上以链表形式存储,每个 Hook 节点包含当前状态和更新队列:

Fiber.memoizedState → Hook1 → Hook2 → Hook3 → null

每个 Hook 节点:
{
  memoizedState,  // 当前状态值
  baseState,      // 初始状态
  queue,          // 更新队列
  next,           // 指向下一个 Hook
}

为什么 Hooks 有使用规则

  • 只在顶层调用:Hooks 按链表顺序匹配,条件调用会破坏顺序,导致状态错乱
  • 只在函数组件中调用:Hooks 依赖 Fiber 上下文,普通函数中没有 Fiber

快速上手

useState 的实现原理

// 简化的 useState 实现
function useState(initialState) {
  // 获取或创建 Hook 节点
  const hook = mountWorkInProgressHook();

  // 初始化状态
  hook.memoizedState = initialState;

  // 创建更新队列
  hook.queue = {
    pending: null, // 待处理的更新
    dispatch: null, // dispatch 函数
    lastRenderedState: initialState, // 上次渲染的状态
  };

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

  return [hook.memoizedState, dispatch];
}

dispatch 的实现

// dispatch 函数:将更新加入队列并调度渲染
function dispatchSetState(fiber, queue, action) {
  // 创建更新对象
  const update = {
    action, // 新值或更新函数
    lane: requestUpdateLane(), // 优先级
    next: null, // 指向下一个更新(环形链表)
  };

  // 将更新加入队列(环形链表)
  const pending = queue.pending;
  if (pending === null) {
    update.next = update; // 指向自己
  } else {
    update.next = pending.next;
    pending.next = update;
  }
  queue.pending = update;

  // 调度渲染
  scheduleUpdateOnFiber(fiber, lane);
}

详细用法

useEffect 的实现原理

// 简化的 useEffect 实现
function useEffect(create, deps) {
  const hook = mountWorkInProgressHook();

  // 存储副作用信息
  hook.memoizedState = {
    create, // 副作用函数
    deps, // 依赖数组
    destroy: undefined, // 清理函数
  };

  // 标记 Fiber 有 Passive 副作用
  currentlyRenderingFiber.flags |= PassiveEffect;
}

// 更新时的 useEffect
function updateEffect(create, deps) {
  const hook = updateWorkInProgressHook();

  // 比较依赖是否变化
  const prevDeps = hook.memoizedState.deps;
  if (areHookInputsEqual(deps, prevDeps)) {
    // 依赖未变化,跳过
    return;
  }

  // 依赖变化,更新副作用
  hook.memoizedState = { create, deps, destroy: undefined };
  currentlyRenderingFiber.flags |= PassiveEffect;
}

useRef 的实现原理

// useRef 本质上是一个始终返回同一对象的 Hook
function useRef(initialValue) {
  const hook = mountWorkInProgressHook();

  // 创建一个可变对象,跨渲染保持引用
  hook.memoizedState = { current: initialValue };
  return hook.memoizedState;
}

// 更新时直接返回同一对象
function updateRef(initialValue) {
  const hook = updateWorkInProgressHook();
  return hook.memoizedState;
}

// 这就是为什么修改 ref.current 不会触发重渲染
// React 不追踪 ref 的变化,只保持引用不变

useMemo 和 useCallback 的实现

// useMemo:缓存计算结果
function useMemo(nextCreate, deps) {
  const hook = mountWorkInProgressHook();
  const nextValue = nextCreate();
  hook.memoizedState = [nextValue, deps];
  return nextValue;
}

function updateMemo(nextCreate, deps) {
  const hook = updateWorkInProgressHook();
  const [prevValue, prevDeps] = hook.memoizedState;

  if (areHookInputsEqual(deps, prevDeps)) {
    return prevValue; // 依赖未变,返回缓存值
  }

  const nextValue = nextCreate();
  hook.memoizedState = [nextValue, deps];
  return nextValue;
}

// useCallback 本质上是 useMemo 的特例
function useCallback(callback, deps) {
  return useMemo(() => callback, deps);
}

常见场景

理解闭包陷阱

// 闭包陷阱:事件处理器中捕获了旧的状态值
function Counter() {
  const [count, setCount] = useState(0);

  function handleClick() {
    // 这里的 count 是渲染时的快照,不是最新值
    setTimeout(() => {
      console.log(count); // 可能是旧值
    }, 1000);
  }

  // 解决方案一:使用函数式更新
  function handleClickFixed() {
    setTimeout(() => {
      setCount((prev) => prev + 1); // 基于最新状态更新
    }, 1000);
  }

  // 解决方案二:使用 ref 保持最新值
  const countRef = useRef(count);
  countRef.current = count;

  function handleClickWithRef() {
    setTimeout(() => {
      console.log(countRef.current); // 始终是最新值
    }, 1000);
  }
}

理解批量更新

// React 18 自动批量更新:所有状态更新合并为一次渲染
function handleClick() {
  setCount((c) => c + 1); // 不会立即渲染
  setFlag((f) => !f); // 不会立即渲染
  setName('张三'); // 不会立即渲染
  // 三次更新合并为一次渲染
}

// 在 React 17 中,只有 React 事件处理器内才会批量更新
// setTimeout 中的更新不会批量处理
// React 18 中所有场景都自动批量更新

注意事项

  • Hooks 的调用顺序必须稳定,不能在条件语句、循环或嵌套函数中调用
  • useEffect 的清理函数在下次 effect 执行前或组件卸载时调用
  • useState 的函数式更新可以避免闭包陷阱,应优先使用
  • useRef 修改 current 不会触发重渲染,适合存储不参与渲染的可变值
  • useMemo 和 useCallback 应在性能分析后使用,不要过度优化
  • React 18 中所有更新都自动批量处理,不再需要 unstable_batchedUpdates

进阶用法

自定义 Hook 的底层原理

// 自定义 Hook 只是复用 Hooks 逻辑的函数
// 调用自定义 Hook 时,其中的 Hooks 会被添加到当前 Fiber 的链表中
function useWindowSize() {
  const [size, setSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight,
  });

  useEffect(() => {
    function handleResize() {
      setSize({
        width: window.innerWidth,
        height: window.innerHeight,
      });
    }
    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);

  return size;
}

// 使用时,useState 和 useEffect 被添加到调用组件的 Fiber 链表中
function App() {
  const size = useWindowSize(); // Hooks 被合并到 App 的链表
  return (
    <div>
      {size.width} x {size.height}
    </div>
  );
}

useSyncExternalStore 的实现

import { useSyncExternalStore } from 'react';

// 用于订阅外部数据源,确保并发模式下的数据一致性
function useOnlineStatus() {
  return useSyncExternalStore(
    // subscribe:订阅函数
    (callback) => {
      window.addEventListener('online', callback);
      window.addEventListener('offline', callback);
      return () => {
        window.removeEventListener('online', callback);
        window.removeEventListener('offline', callback);
      };
    },
    // getSnapshot:获取当前值
    () => navigator.onLine,
    // getServerSnapshot:服务端渲染时的值
    () => true
  );
}

Hooks 链表存储

基本写法:组件对应 Fiber 的 memoizedState <fiber>.memoizedState = <hook链表>

// 每次渲染按顺序构建链表
fiber.memoizedState = hook1;
hook1.next = hook2;

基本写法:Hook 对象结构 type <Hook> = { memoizedState, baseState, baseQueue, queue, next }

// Hook 节点字段
{
  memoizedState: 0,
  queue: { pending: null },
  next: nextHook
}

Hook 调用顺序约束

基本写法:必须保证每次渲染调用顺序一致 use<A>(); use<B>();

// 顺序错乱会导致状态错位
function App() {
  const [a] = useState(0);
  const [b] = useState(0);
}

基本写法:禁止在条件分支中调用 if (<条件>) useState(); // 错误

// 正确做法条件放在 hook 之后
const [v, setV] = useState(0);
if (cond) setV(1);

mount 与 update 两套实现

基本写法:首次挂载走 mount 队列 const <dispatch> = mountState(<初值>)

// 初次创建 hook 并初始化
const [state, dispatch] = mountState(initial);

基本写法:更新走 update 队列 const <dispatch> = updateState()

// 复用已有 hook 处理更新
const [state, dispatch] = updateState();

useState 实现

基本写法:dispatchAction 创建 update function <dispatchAction>(<hook>, <action>) { <update>.next = <update>; }

// 环形链表追加 update
const update = { action };
update.next = update;
hook.queue.pending = update;

基本写法:render 时遍历 queue 计算新状态 while (<update> !== <first>) { <state> = <reducer>(<state>, <update>.action); }

// 逐一应用 action 得到最新 state
let newState = state;
while (update) {
  newState = reducer(newState, update.action);
  update = update.next;
}

useReducer 实现

基本写法:与 useState 类似但用 reducer const [state, dispatch] = mountReducer(<reducer>, <初值>)

// dispatch 调用 reducer 计算新状态
dispatch({ type: 'INC' });

useEffect 实现

基本写法:effect 对象挂在 updateQueue <hook>.updateQueue.lastEffect = <effect>

// effect 形成环形链表
effect.next = effect;
hook.updateQueue.lastEffect = effect;

基本写法:effect 包含 create 与 destroy type <Effect> = { tag, create, destroy, deps, next }

// create 是副作用函数 destroy 是清理函数
{
  create: () => subscribe(),
  destroy: () => unsubscribe(),
  deps: [id]
}

deps 依赖比较

基本写法:浅比较决定是否执行 effect if (!<shallowEqual>(<prevDeps>, <nextDeps>)) <执行>

// Object.is 逐项比较
const areEqual = prevDeps.every((d, i) => Object.is(d, nextDeps[i]));

useRef 实现

基本写法:ref 对象首次创建后保持引用 const <ref> = { current: <初值> }

// ref 直接存入 memoizedState 不参与更新
hook.memoizedState = { current: initial };

useMemo useCallback 实现

基本写法:依赖不变返回缓存值 if (<depsChanged>) <重新计算> else <返回缓存>

// 缓存结果与依赖
if (depsChanged) {
  hook.memoizedState = [factory(), deps];
}
return hook.memoizedState[0];

闭包陷阱成因

基本写法:effect 捕获旧值导致 stale closure useEffect(() => <使用旧值>, [])

// 空依赖导致捕获首次渲染的 count
const [count] = useState(0);
useEffect(() => {
  const id = setInterval(() => console.log(count), 1000);
  return () => clearInterval(id);
}, []);

闭包陷阱解决依赖

基本写法:补全依赖项 useEffect(() => <逻辑>, [<依赖>])

// 加入 count 让每次更新重建 effect
useEffect(() => {
  const id = setInterval(() => console.log(count), 1000);
  return () => clearInterval(id);
}, [count]);

使用 ref 规避闭包

基本写法:ref.current 始终是最新值 const <latest> = useRef(<值>); <latest>.current = <值>;

// 通过 ref 读取最新 count
const countRef = useRef(count);
countRef.current = count;
useEffect(() => {
  const id = setInterval(() => console.log(countRef.current), 1000);
  return () => clearInterval(id);
}, []);

useEffectEvent 规避闭包

基本写法:用 useEffectEvent 抽离非响应式逻辑(React 19.2 正式) const <fn> = useEffectEvent(<回调>)

// 内部始终读取最新 props/state,且不进入依赖数组
const onTick = useEffectEvent(() => console.log(count));
useEffect(() => {
  const id = setInterval(onTick, 1000);
  return () => clearInterval(id);
}, []);

useReducer 解决闭包

基本写法:dispatch 引用稳定不捕获旧值 dispatch({ type: 'INC' })

// dispatch 永远是最新的不进依赖
useEffect(() => {
  const id = setInterval(() => dispatch({ type: 'INC' }), 1000);
  return () => clearInterval(id);
}, [dispatch]);

setState 函数式更新

基本写法:使用函数式更新读取最新 state setCount(c => c + 1)

// 避免依赖外部 count
setInterval(() => setCount(c => c + 1), 1000);

Hook 规则 ESLint 校验

基本写法:eslint-plugin-react-hooks 强制规则 npm i -D eslint-plugin-react-hooks

# 安装后自动检测违反规则的写法
npm install --save-dev eslint-plugin-react-hooks

自定义 Hook 闭包

基本写法:自定义 Hook 也要注意依赖 function use<名称>(<值>) { useEffect(() => <用值>, [<值>]); }

// 内部依赖必须完整
function useTimer(cb) {
  useEffect(() => {
    const id = setInterval(cb, 1000);
    return () => clearInterval(id);
  }, [cb]);
}

useState 惰性初始化

基本写法:传入函数仅首次调用 useState(() => <昂贵计算>)

// 避免每次渲染重复计算
const [data] = useState(() => heavyCompute());

bailout 优化

基本写法:props 与 state 未变跳过渲染 if (<oldProps> === <newProps>) bailout

// 浅比较决定是否跳过子树处理
if (Object.is(prevProps, nextProps)) return bailout;

并发模式下 Hook 行为

基本写法:transition 内 setState 走低优先级 lane startTransition(() => <setState>)

// 标记为非紧急更新
startTransition(() => setList(bigData));

严格模式双重渲染

基本写法:开发环境两次渲染检测副作用 <React.StrictMode> <App/> </React.StrictMode>

// 帮助发现不纯函数副作用
<React.StrictMode><App /></React.StrictMode>

Hook 与 Fiber 关系

基本写法:每次渲染重建 hook 链表 <render> -> <遍历hook链表> -> <执行hook函数>

// 通过 current.memoizedState 复用上次状态
renderHooks(fiber);