跳到主要内容

React Hook

社区来稿 · Liu由用户发布,不代表 batype 本人观点

·1063 字 · 约 3 分钟

React Hook 的原理实现。


React Hook 原理与实现总结

一、React Hook 原理简述

  1. 本质
    React Hook 是通过“链表”结构,将每个 Hook 的状态挂载在当前函数组件对应的 Fiber 节点(FiberNode)的 memoizedState 属性上。
  2. 顺序保证
    每次渲染时,React 通过链表顺序遍历 Hook,确保每个 Hook 状态与调用顺序一一对应。
  3. 更新机制
    每次 setState/useReducer 等触发更新时,都会将更新 action 以 update 节点的形式加入 queue 队列,等到下次渲染时统一处理。

二、Hook 节点结构与字段含义

以 useState/useReducer 为例,每个 Hook 节点结构大致如下:

const hook = {
  memoizedState, // 当前 Hook 的状态值
  baseState,     // 上一次的状态(用于优化和回溯)
  queue,         // 更新队列(存放 setState 产生的 update)
  next,          // 指向下一个 Hook 节点
}

字段详细说明

字段名 含义 可能存在的问题/注意点
memoizedState 当前 Hook 的状态值(如 useState 的 state,useRef 的 current) 1. 状态类型必须与初始值一致 2. 顺序不能变
baseState 上一次的状态(用于优化、回溯、跳过部分 update) 1. 仅在并发模式/多次更新时有用 2. 可能过期
queue 更新队列,存放所有待处理的 update(环状链表) 1. 队列过长可能影响性能 2. 批量更新需谨慎
next 指向下一个 Hook 节点,形成链表 1. Hook 调用顺序不能变 2. 条件 Hook 会出错

queue 字段内部结构

const queue = {
  pending,   // 指向环状链表的最后一个 update 节点
  dispatch,  // setState 函数
  // useReducer 时还会有 lastRenderedReducer, lastRenderedState
}

update 节点结构

const update = {
  action, // setState 传入的值或函数
  next,   // 指向下一个 update,形成环状链表
}

三、常见问题与注意事项

  1. Hook 调用顺序不能变
    • 不能在条件、循环、嵌套函数中调用 Hook,否则链表顺序错乱,状态错位。
  2. 状态类型要一致
    • memoizedState 类型要和初始值一致,否则可能出现类型错误。
  3. 批量更新与性能
    • queue 队列过长时,批量处理所有 update 可能带来性能问题。
  4. 并发模式下的状态一致性
    • baseState、queue 设计是为了解决并发渲染下的状态一致性问题,但极端情况下可能出现“回滚”或“丢失更新”。
  5. useRef 特例
    • useRef 只用 memoizedState 存储 current,不涉及 queue。

四、Hook 链表结构图


五、简要流程

  1. 首次渲染:依次创建 Hook 节点,挂到 FiberNode.memoizedState 链表上。
  2. 更新时:遍历链表,依次处理每个 Hook 的 queue 队列,更新 memoizedState。
  3. setState:创建 update 节点,插入 queue.pending 环状链表,下次渲染时统一处理。

六、参考代码片段

// Hook 节点
const hook = {
  memoizedState: 0, // 当前 state
  baseState: 0,     // 上一次 state
  queue: {
    pending: null,  // update 环状链表
    dispatch: null, // setState
  },
  next: null,       // 下一个 Hook
}

为什么 queue.pending 采用环状链表,以及为什么更新时需要遍历整个链表,而不是只处理最后一项。


1. queue.pending 是什么?

  • queue.pending 是一个环状单向链表(circular linked list),每个节点代表一次 setState 产生的更新(update)。
  • 每次 setState,都会把新的 update 节点插入到链表中,并让 queue.pending 指向最新的那个节点。

2. 为什么不是只处理最后一项?

1)React 支持多次 setState 合并批量处理

  • 在一次事件循环中,可能会多次调用 setState(比如在同一个事件处理函数里多次 setState)。
  • React 需要保证所有的更新都被依次处理,而不是只处理最后一次。
  • 比如:
setState(x => x + 1);
setState(x => x + 1);
setState(x => x + 1);

你期望 state 最终加 3,而不是只加 1。

2)每个 update 可能是函数式更新,必须依次应用

  • 如果 update 是函数(如 setState(x => x + 1)),每个更新都依赖上一次的结果,不能跳过。
  • 只有依次遍历链表,把每个 update 应用到 state 上,才能得到正确的最终结果。

3. 为什么用环状链表?

  • 环状链表方便在 O(1) 时间内插入新节点(只需操作 queue.pending 指针)。
  • 处理时,从 queue.pending.next 开始遍历,直到回到 queue.pending,能保证所有更新都被处理且顺序正确。

4. 伪代码说明

// 插入新 update
function enqueueUpdate(queue, update) {
  if (queue.pending === null) {
    update.next = update; // 自己指向自己
  } else {
    update.next = queue.pending.next;
    queue.pending.next = update;
  }
  queue.pending = update; // 指向最新的 update
}

// 处理所有 update
function processQueue(queue, baseState) {
  let newState = baseState;
  let first = queue.pending?.next;
  let update = first;
  do {
    newState = typeof update.action === 'function'
      ? update.action(newState)
      : update.action;
    update = update.next;
  } while (update !== first);
  queue.pending = null;
  return newState;
}

5. 总结

  • 不能只处理最后一项,因为每个 setState 都可能影响最终结果,必须全部依次处理。
  • 环状链表方便插入和遍历,保证所有更新都能被顺序处理。
  • 这样才能实现 React 的批量更新、函数式 setState 等特性,保证状态一致性。

如果你还想了解更底层的源码细节或有其他相关问题,欢迎继续提问!