前端工程Technical Deep Dive

React Hooks 源码解析 —— useState、useEffect 原理深度剖析

发布时间2026/03/29
分类前端工程
预计阅读2 分钟
作者吴长龙
*

为什么需要理解 Hooks 源码

01.内容

为什么需要理解 Hooks 源码

Hooks 2019 年引入 React,彻底改变了组件写法。但很多人只是"会用",不清楚为什么有时候会出现令人困惑的行为。本文将从源码角度解析 Hooks 的工作原理。

Hooks 的基本结构

React 内部用链表存储 Hook 信息。每个组件对应一个 Hooks 链表:

javascript snippetjavascript
// 简化版 Hook 结构
{
  memoizedState: 缓存的状态值,
  baseState: 基础状态,
  queue: 待执行的更新队列,
  next: 下一个 Hook,
  // ...
}

关键点:Hooks 按调用顺序存储,这就是为什么 Hooks 不能在条件语句中调用。

useState 原理

#### 基本实现

javascript snippetjavascript
function useState(initialState) {
  let hook = currentlyRenderingComponent.hooks[index];
  
  if (!hook) {
    // 首次渲染:创建 Hook
    hook = {
      memoizedState: initialState,
      queue: { pending: null },
      next: null,
    };
    if (!firstHook) {
      firstHook = hook;
    } else {
      hooksTail.next = hook;
    }
    hooksTail = hook;
  }
  
  // 更新逻辑
  let baseState = hook.memoizedState;
  if (hook.queue.pending) {
    let update = hook.queue.pending;
    baseState = typeof update.action === 'function' 
      ? update.action(baseState) 
      : update.action;
    hook.queue.pending = null;
    hook.memoizedState = baseState;
  }
  
  return [baseState, dispatchAction.bind(null, hook.queue)];
}

#### 为什么 useState 调用顺序很重要

jsx snippetjsx
// 错误示例
function MyComponent() {
  const [count, setCount] = useState(0);
  
  if (count > 0) {
    const [name, setName] = useState(''); // 条件调用 Hook!
  }
  
  return <div>{name}</div>;
}

如果 count 为 0,组件渲染时只调用了 useState(0)。但当 count 变为 1 时,会调用两次 useState,React 就会混乱——它会尝试用第二个 Hook 的数据结构去读取 name,导致 bug。

这就是 Hook 调用规则的底层原因。

useEffect 原理

#### 差异于 useLayoutEffect

javascript snippetjavascript
function useEffect(create, deps) {
  // 标记为需要异步调度的副作用
  currentFiber.flags |= UpdateEffect | PassiveEffect;
  hook.memoizedState = pushEffect(
    HookHasEffect, 
    create, 
    deps
  );
}

function useLayoutEffect(create, deps) {
  // 标记为需要同步调度的副作用
  currentFiber.flags |= UpdateEffect;
  hook.memoizedState = pushEffect(
    HookHasEffect, 
    create, 
    deps
  );
}

关键区别在于 flags 不同:

  • useEffect:额外添加 PassiveEffect,告诉 React 在渲染完成后异步执行
  • useLayoutEffect:同步执行,在 DOM 更新前运行

#### 依赖项比较

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

React 使用 Object.is 进行依赖比较(类似 ===,但 NaN === NaN 为 true,+0 !== -0)。

useMemo 与 useCallback

javascript snippetjavascript
function useMemo(nextCreate, deps) {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  
  const nextValue = nextCreate();
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}

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

重要:useMemo 的 nextCreate 是函数返回值,而不是函数本身。如果你写 useMemo(() => fn, deps),每次渲染都会执行 fn(),但结果可能被缓存。

闭包陷阱

jsx snippetjsx
function Counter() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    const timer = setInterval(() => {
      console.log('Count:', count); // 这里的 count 永远是 0!
    }, 1000);
    return () => clearInterval(timer);
  }, []); // 空依赖,只运行一次
  
  return <div onClick={() => setCount(c => c + 1)}>{count}</div>;
}

问题:Effect 只在首次渲染后执行,此时 count 是 0。定时器闭包捕获了 count = 0,之后永远读不到新值。

解决方案

jsx snippetjsx
// 方案1:使用函数式更新
useEffect(() => {
  const timer = setInterval(() => {
    setCount(c => c + 1); // 使用最新状态
  }, 1000);
  return () => clearInterval(timer);
}, []);

// 方案2:加入依赖
useEffect(() => {
  const timer = setInterval(() => {
    console.log('Count:', count);
  }, 1000);
  return () => clearInterval(timer);
}, [count]); // count 变化时重新创建定时器

自定义 Hook

自定义 Hook 不过是复用状态逻辑的函数

javascript snippetjavascript
function useToggle(initialValue = false) {
  const [value, setValue] = useState(initialValue);
  const toggle = useCallback(() => setValue(v => !v), []);
  return [value, toggle];
}

// 使用
function App() {
  const [isOn, toggle] = useToggle();
  return <button onClick={toggle}>{isOn ? 'ON' : 'OFF'}</button>;
}

自定义 Hook 的核心是组合多个基础 Hook,创造新的抽象。

性能优化建议

  • useMemo 避免不必要的计算
jsx snippetjsx
   const expensive = useMemo(() => compute(a, b), [a, b]);
  • useCallback 稳定回调函数
jsx snippetjsx
   const handleClick = useCallback(() => {
     doSomething(count);
   }, [count]);
  • useRef 存储可变值,不触发重渲染
jsx snippetjsx
   const ref = useRef(null);
   ref.current = '变化但不触发重渲染';
  • 合理设置依赖项

依赖项过多会导致频繁重建,依赖项过少会导致闭包陷阱。

总结

理解 Hooks 源码能帮助我们:

  • 避免常见 bug(如条件调用 Hook、闭包陷阱)
  • 更合理地选择 Hook 方案
  • 在性能优化时有的放矢
  • 更好地理解 React 的设计哲学