今天看到两个API感觉蛮有趣useRef与useState

官方描述不再赘述,个人理解:useRef可以操作这个Dom,使用元素上的各种方法,但是useState相当于一个值存在组件中,可以改变值,但是ref只能通过current改变值。这点引起了我的好奇,useRef到底如何实现的呢?

一、前置知识:Hook 底层基础

1. 核心依赖

  1. Fiber 架构:每个函数组件对应一个 Fiber 节点,用于存储组件状态、DOM 信息和 Hook 链表。
  2. Hook 链表:组件的所有 Hook(useState/useRef 等)以单向链表形式存储在 fiber.memoizedState 中,是 Hooks 持久化的核心。

2. 关键数据结构

// 简化 Hook 节点结构

type Hook = {

  memoizedState: any; // 存储 Hook 核心数据(useRef 存容器,useState 存状态+队列)

  next: Hook | null;   // 指向下一个 Hook 节点(形成链表)

  tag: HookTag;        // Hook 类型标记(区分 useState/useRef

};

// 全局追踪变量(React 内部使用)

let currentlyRenderingFiber = null; // 当前渲染的 Fiber 节点

let workInProgressHook = null;      // 当前遍历的 Hook 节点

3. 核心原则

  1. Hook 调用顺序不可变:链表按 Hook 调用顺序排列,必须在组件顶层调用(不能在条件 / 循环中),否则会导致链表错乱。

二、useRef 源码解析

1. 功能定位

创建持久化引用容器,用于存储 DOM 引用或非渲染依赖数据,修改不触发组件重渲染。

2. 核心源码(简化版)

function useRef(initialValue) {

  // 1. 获取/创建当前 Hook 节点

  const hook = updateWorkInProgressHook(HookTag.useRef);

  // 2. 首次渲染初始化引用容器

  if (hook.memoizedState === undefined) {

    hook.memoizedState = { current: initialValue };

  }

  // 3. 返回容器(每次渲染返回同一个对象)

  return hook.memoizedState;

}

3. 关键原理

  1. 持久化特性:引用容器存储在 hook.memoizedState 中,组件重新渲染时直接复用该对象(引用不变)。
  2. 不触发重渲染:修改 current 属性仅改变对象内部值,未改变 memoizedState 的引用,React 不会触发重渲染。
  3. 初始化逻辑:仅首次渲染时创建容器,后续渲染直接返回已存在的容器。

4. 适用场景

  1. 存储 DOM 元素引用(如 ref={inputRef})。
  2. 存储不需要触发渲染的持久化数据(如定时器 ID、第三方库实例)。

三、useState 源码解析

1. 功能定位

创建组件状态(state)和状态更新函数(setState),修改状态会触发组件重渲染,支持批量更新和函数式更新。

2. 核心源码(简化版)

(1)主函数

function useState(initialState) {

  // 1. 获取/创建当前 Hook 节点

  const hook = updateWorkInProgressHook(HookTag.useState);

  // 2. 首次渲染初始化:状态 + 更新队列(循环链表)

  if (hook.memoizedState === undefined) {

    const initialValue = typeof initialState === "function"

      ? initialState()

      : initialState;

    hook.memoizedState = {

      state: initialValue,

      queue: { pending: null } // 存储待执行的更新任务

    };

  }

  // 3. 解构状态和更新队列

  const { state, queue } = hook.memoizedState;

  // 4. 创建 setState 函数

  const setState = (newState) => {

    // 创建更新任务

    const update = { action: newState, next: null };

    // 加入循环链表队列

    if (queue.pending === null) update.next = update;

    else {

      update.next = queue.pending.next;

      queue.pending.next = update;

    }

    queue.pending = update;

    // 触发组件重新渲染

    scheduleUpdateOnFiber(currentlyRenderingFiber);

  };

  return [state, setState];

}

(2)更新队列处理(组件重渲染时)

function processHookUpdateQueue(hook) {

  const { queue, state: oldState } = hook.memoizedState;

  let newState = oldState;

  if (queue.pending !== null) {

    queue.pending = null;

    let currentUpdate = queue.pending.next;

    // 遍历所有更新任务,计算新状态

    do {

      const action = currentUpdate.action;

      newState = typeof action === "function" ? action(newState) : action;

      currentUpdate = currentUpdate.next;

    } while (currentUpdate !== queue.pending);

    // 更新状态

    hook.memoizedState.state = newState;

  }

  return newState;

}

3. 关键原理

(1)状态存储
  1. memoizedState 存储对象:{ state: 当前状态, queue: 更新队列 }
  2. 更新队列是循环链表,用于批量存储多次 setState 调用。
(2)setState 工作流程
  1. 调用 setState → 创建更新任务 → 加入循环链表。
  2. 触发组件重渲染 → 遍历更新队列 → 计算新状态(处理函数式更新)。
  3. 用新状态替换旧状态 → 组件用新状态重新渲染。
(3)核心特性
  1. 批量更新:多次 setState 合并为一次渲染(React 18 默认所有场景支持)。
  2. 函数式更新setState(prev => prev + 1),解决状态依赖旧值的问题。
  3. 懒初始化useState(() => 复杂计算),仅首次渲染执行初始化函数。

4. 适用场景

  1. 存储组件渲染依赖的状态(如表单值、弹窗显示状态、列表数据)。
  2. 需要通过状态变化触发 UI 更新的场景。

四、useRef 与 useState 核心区别

对比维度

useRef

useState

存储内容

引用容器 { current: ... }

状态值 + 循环更新队列

修改触发重渲染

不触发(仅改 current 内部值)

触发(调用 setState 加入队列)

更新方式

直接修改 current(同步)

批量更新 / 函数式更新(异步调度)

引用稳定性

每次渲染返回同一个容器对象

状态值可能变化,setState 引用不变

初始化支持

仅直接传初始值(无函数式)

支持直接传值 / 函数式懒初始化

核心用途

存储 DOM 引用、非渲染数据

存储渲染依赖的组件状态

五、常见易错点与注意事项

1. Hook 调用规则

  1. ❌ 错误:在 if/for 等条件 / 循环中调用 Hook。
  2. ✅ 正确:在组件顶层、函数组件或自定义 Hook 顶层调用。
  3. 原因:Hook 链表依赖调用顺序,顺序错乱会导致状态复用失败。

2. useState 常见问题

  1. 状态更新异步setState 是异步调度,不能立即获取新状态(需用 useEffect 监听)。
  2. 状态依赖旧值:连续更新依赖前一次状态时,必须用函数式更新(如 setCount(prev => prev + 1))。
  3. 批量更新失效:需要强制同步更新时,用 ReactDOM.flushSync(() => setState(newVal))

3. useRef 常见问题

  1. current 赋值时机:存储 DOM 时,需在组件挂载后访问 current(可在 useEffect 中获取)。
  2. 避免存储渲染依赖数据:若数据变化需要触发 UI 更新,应使用 useState 而非 useRef。

六、总结

  1. 底层共性:useRef 和 useState 均基于 Fiber 节点的 Hook 链表实现,依赖调用顺序保证状态持久化。
  2. 核心差异:useRef 是 “无渲染副作用的持久化容器”,useState 是 “带更新队列的状态管理工具”。
  3. 使用原则:根据是否需要触发 UI 更新选择 —— 需要更新 UI 用 useState,仅需持久化数据用 useRef。

(注:文档部分内容可能由 AI 生成)

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐