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 等特性,保证状态一致性。

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