深入 useState、useEffect 的底层实现

一、前言

React Hooks 的出现极大地简化了函数组件的状态管理和副作用处理,但其内部的精妙设计往往隐藏在日常使用的 API 背后。本文旨在揭开这些"幕后"的秘密,带领读者从源码层面理解 useState、useEffect 等核心 Hook 的工作原理。

我们将从最基础的 Fiber 节点和 Hook 链表等内存布局开始,逐步深入到 useState 的初始化与更新流程、useEffect 的依赖比较与副作用调度,以及 React 内部的更新队列和优先级调度机制。

二、函数组件的内存布局基础

要理解 Hooks 如何工作,核心是先搞懂函数组件的内存承载结构------ 所有状态、副作用都依赖「Fiber 节点」和「Hook 链表」这两个核心载体存储。

2.1 核心数据结构总览:它们如何协同工作?

在深入细节之前,我们先来建立一个整体的认知框架。想象一下,React 在管理你的函数组件时,就像在搭建一个精密的"信息管理系统"。这个系统主要由以下几个核心"部件"组成:

  1. Fiber 节点(组件的"档案袋"):

    • 每个 React 组件(无论是函数组件还是类组件)在 React 内部都有一个对应的 Fiber 节点。你可以把它理解为这个组件的"专属档案袋",里面记录了组件的所有重要信息,比如它的类型、属性(props)、在组件树中的位置,以及最重要的------它的状态 和副作用。
    • 对于函数组件而言,Fiber 节点 的一个关键字段 memoizedState,就像是这个档案袋的"入口标签",它指向了该组件所有 Hook 的起始位置。
  2. Hook 链表(状态和副作用的"清单"):

    • 当你在函数组件中调用 useState、useEffect 等 Hook 时,React 并不会把它们独立存储。相反,它会将这些 Hook 组织成一个单向链表 ,挂载在对应的 Fiber 节点 上。
    • 这个 Hook 链表 就像是组件"档案袋"里的一份详细"清单",每一项(每一个 Hook 节点)都记录着一个 useState 的当前值,或者一个 useEffect 的副作用函数和依赖项等信息。
    • 链表的顺序非常重要,它严格按照你在组件中调用 Hook 的顺序排列。
  3. 更新队列(状态变化的"待办事项列表"):

    • 每个 useState Hook 内部,都维护着一个独立的 更新队列。当你调用 setState 来更新状态时,React 并不会立即改变 Hook 节点上的状态值,而是会创建一个 Update 对象(一个"待办事项"),并把它添加到这个 更新队列 中。
    • 这个 更新队列 就像是每个 useState 的"专属待办事项列表",里面记录了所有等待被处理的状态更新请求。React 会在合适的时机,按照一定的优先级规则,逐一处理这些"待办事项",最终计算出最新的状态。

它们如何协同工作?

  • Fiber 节点 是容器 ,它持有组件的所有信息,并通过 memoizedState 字段指向 第一个 Hook 节点。
  • Hook 链表 是内容 ,它存储了组件中所有 useState 和 useEffect 等 Hook 的具体数据,并通过 next 指针串联起来。
  • 更新队列 是机制 ,它附着在每个 useState 的 Hook 节点上,负责管理 和调度状态的变更。

理解这三者之间的关系,是理解 React Hooks 内部机制的关键。接下来,我们将逐一深入这些数据结构的细节。

2.2 Fiber 节点与 memoizedState

函数组件本身是无实例的纯函数,无法像类组件那样用 this 存储状态 ------ 所有状态、Hooks 信息,都寄生在组件对应的 Fiber 节点 中。可以把 Fiber 节点理解为 "函数组件的内存容器",其关键字段直接决定了状态的存储与读取逻辑。

Fiber 节点的核心结构

Fiber 节点与函数组件状态相关的核心字段如下(剔除调度、树结构等非核心字段):

javascript 复制代码
function FiberNode(tag, pendingProps, key, mode) {
  // 1. 状态存储核心:函数组件的 Hooks 链表入口
  this.memoizedState = null; // 关键!指向第一个 Hook 节点(如 useState、useEffect 节点)
  // 2. 组件接收的 props:上次渲染时使用的 props(用于依赖对比)
  this.memoizedProps = null;
  // 3. 更新队列:管理函数组件的副作用、事件等(如 useEffect 队列)
  this.updateQueue = null;
  this.alternate = null; // 4. 双缓冲关联:指向另一棵树的 Fiber 节点。React 内部维护"两棵树":一棵是当前屏幕上显示的(current),另一棵是在后台构建的(workInProgress)。`alternate` 字段就是这两棵树之间相互连接的桥梁,用于在更新过程中切换和复用 Fiber 节点。
}

memoizedState:Hook 链表的入口

对于函数组件,memoizedState 字段具有特殊意义------它是整个 Hook 链表的入口点。与类组件不同,函数组件的 memoizedState 不直接存储状态对象,而是指向第一个 Hook 节点。

javascript 复制代码
// 函数组件的 memoizedState 指向 Hook 链表头部
fiber.memoizedState = firstHook; // Hook 链表的第一个节点

// 类组件的 memoizedState 直接存储状态对象
classComponentFiber.memoizedState = { count: 0, name: "React" };

渲染过程中的状态管理

函数组件每次渲染(首次 / 重渲染),都会通过 renderWithHooks 函数初始化内存结构,核心是 "重置链表入口 + 选择调度器":

javascript 复制代码
export function renderWithHooks(
  current, // 当前 Fiber 节点(已渲染到屏幕的)
  workInProgress, // 工作 Fiber 节点(本次要渲染的)
  Component, // 函数组件本身
  props, // 组件接收的 props
  nextRenderLanes // 渲染优先级
) {
  // 1. 绑定全局变量:让 Hooks 能找到当前正在渲染的 Fiber 节点
  currentlyRenderingFiber = workInProgress;
  // 2. 重置 Hooks 链表入口:避免上次渲染的链表残留
  workInProgress.memoizedState = null;
  // 3. 选择 Hooks 调度器:首次渲染用 Mount 调度器,重渲染用 Update 调度器
  ReactSharedInternals.H =
    current === null
      ? HooksDispatcherOnMount // 首次渲染:创建新 Hook 节点
      : HooksDispatcherOnUpdate; // 重渲染:复用已有 Hook 节点
  // 4. 执行函数组件:触发 useState、useEffect 调用,构建/复用 Hooks 链表
  const children = Component(props);
  // 5. 清理全局变量,完成渲染
  finishRenderingHooks(current, workInProgress, Component);
  return children;
}

简单说:renderWithHooks 是 "内存初始化的开关",决定了 Hooks 链表是 "新建" 还是 "复用",是连接函数组件与 Fiber 节点的核心桥梁。

2.3 Hook 链表与更新队列的存储机制

Hook 节点的内存结构

无论是什么类型的 Hook(useState/useEffect),都共享同一个基础结构,核心字段如下:

javascript 复制代码
type Hook = {
  memoizedState: any, // 1. 当前 Hook 的"状态值"。对于 `useState`,它存储的是最新的状态值;对于 `useEffect`,它存储的是副作用函数和依赖数组等信息。
  baseState: any, // 2. "基础状态"。在处理状态更新时,`baseState` 记录了上一次成功提交(commit)时的状态。它与 `baseQueue` 配合,用于在跳过低优先级更新时,确保状态计算的正确性。
  baseQueue: Update<any> | null, // 3. "基础更新队列"。存储了上一次渲染周期中,因为优先级不足而被跳过,但仍需在未来处理的更新。它是一个环形链表,与 `baseState` 一起保证了状态更新的连贯性。
  queue: any, // 4. "当前更新队列"。存储了所有待处理的更新(`Update` 对象)。对于 `useState`,它包含了 `setState` 调用产生的更新。这个队列也是一个环形链表。
  next: Hook | null, // 5. "链表指针"。指向下一个 Hook 节点,将所有 Hook 串联成一个单向链表。
};

不同 Hook 的差异,仅体现在 memoizedState 和 queue 的具体内容(如 useEffect 的 memoizedState 存副作用函数和依赖,useState 存具体数值)。

链表的构建与遍历

以一个包含多个 Hook 的函数组件为例:

javascript 复制代码
function MyComponent() {
  const [count, setCount] = useState(0); // Hook1
  const [name, setName] = useState("React"); // Hook2
  useEffect(() => {
    // Hook3
    document.title = `Count: ${count}`;
  }, [count]);

  return (
    <div>
      {count} - {name}
    </div>
  );
}

其内存布局如下:

javascript 复制代码
// Fiber 节点:memoizedState 指向第一个 Hook 节点
MyComponentFiber {
  memoizedState: Hook1 (useState(count)), // 链表入口
  memoizedProps: { ... }, // 组件接收的 props
  updateQueue: { ... }, // 副作用队列
  alternate: null
}

// Hook1:useState(count) 节点
Hook1 {
  memoizedState: 0, // 当前状态值:count = 0
  baseState: 0,
  queue: { ... }, // setCount 的更新队列
  next: Hook2 (useState(name)) // 指向第二个 Hook
}

// Hook2:useState(name) 节点
Hook2 {
  memoizedState: "React", // 当前状态值:name = "React"
  baseState: "React",
  queue: { ... }, // setName 的更新队列
  next: Hook3 (useEffect) // 指向第三个 Hook
}

// Hook3:useEffect 节点
Hook3 {
  memoizedState: { // 存副作用相关信息
    create: () => { document.title = `Count: ${count}`; }, // 副作用函数
    destroy: null, // 清理函数(本例无)
    deps: [0] // 依赖数组:[count]
  },
  baseState: null,
  queue: null,
  next: null // 最后一个 Hook,next 为 null
}

链表顺序严格遵循 Hooks 的调用顺序(Hook1→Hook2→Hook3),这也是 "不能在条件语句中写 Hooks" 的根本原因(会打乱链表顺序); 每个 Hook 节点的 next 指针是 "链式存储" 的关键,遍历链表时通过 next 依次访问所有 Hook; Fiber 节点仅需持有链表的 "头指针"(memoizedState),就能找到所有 Hook 节点,实现高效的状态管理。

FunctionComponentUpdateQueue 结构

除了 Hooks 链表,函数组件的 Fiber 节点还通过 updateQueue 字段存储辅助信息,其类型为 FunctionComponentUpdateQueue,核心用于管理副作用和事件:

javascript 复制代码
// FunctionComponentUpdateQueue 定义
export type FunctionComponentUpdateQueue = {
  lastEffect: Effect | null, // 指向最后一个 Effect 节点(如 useEffect 队列)
  events: Array<EventFunctionPayload> | null, // 事件处理队列(如 useEffectEvent)
  stores: Array<StoreConsistencyCheck> | null, // 状态一致性检查
  memoCache: MemoCache | null, // 记忆化缓存(如 useMemo/useCallback 的缓存)
};

它相当于 Hooks 链表的 "辅助仓库",专门存储链表之外的副作用调度信息,与 Hooks 链表配合实现完整的状态与副作用管理。

三、useState 的底层实现机制

useState 是 React Hooks 中最基础也最常用的 Hook 之一,它允许你在函数组件中"存储"和"更新"状态。但它背后是如何工作的呢?在 React 内部,useState 的行为会根据组件是首次渲染(挂载)还是后续渲染(更新)而有所不同。

简单来说,当你的函数组件第一次被渲染时,useState 会走一套"初始化"流程;而当组件因为状态变化需要重新渲染时,useState 则会走一套"更新"流程。在整个过程中,React 还会利用一个"更新队列"和"优先级调度"机制,来确保状态更新的有序性和高效性。

3.1 mountState:初始化状态管理

当一个函数组件首次被渲染到屏幕上时,React 会进入一个"挂载"阶段。在这个阶段,useState 会被一个特殊的"Hooks 分发器"(HooksDispatcherOnMount)拦截,并将其内部逻辑指向 mountState 函数。

3.1.1 发生时机

  • 首次渲染函数组件时 :这是 mountState 登场的唯一时机。想象一下,你的组件就像一个新开张的商店,mountState 负责为它准备好所有必要的"货架"(Hook 节点)和"库存管理系统"(更新队列)。

3.1.2 初始值如何处理

当你调用 useState 时,可以传入一个初始值,这个初始值可以是具体的数据(如 useState(0)),也可以是一个函数(如 useState(() => computeInitialValue()))。

  • 懒初始化(Lazy Initialization):

    • 如果你传入的是一个函数(例如 useState(() => heavyInit())),React 不会立即执行这个函数,而是在真正需要初始值的时候才去调用它。这种方式被称为"懒初始化",当你的初始值计算成本很高时,可以避免不必要的性能开销。
    javascript 复制代码
    function mountStateImpl(initialState) {
      const hook = mountWorkInProgressHook(); // 创建 Hook 节点
      if (typeof initialState === "function") {
        const initialStateInitializer = initialState;
        initialState = initialStateInitializer(); // 第一次调用,获取初始值
        if (shouldDoubleInvokeUserFnsInHooksDEV) {
          // 仅在 DEV 严格模式下
          setIsStrictModeForDevtools(true);
          try {
            initialStateInitializer(); // 第二次调用,检查副作用
          } finally {
            setIsStrictModeForDevtools(false);
          }
        }
      }
      // ... 后续会用这个 initialState 来设置 hook.memoizedState 和 hook.baseState
      return hook;
    }

3.1.3 Hook 节点与更新队列的建立

在 mountStateImpl 函数中,React 会为当前的 useState 调用创建一个内部的 Hook 节点 (hook 对象),并为它配备一个 更新队列 (queue 对象)。

  • Hook 节点(hook):

    • memoizedState:这是 Hook 节点中最重要的字段,它存储着当前组件渲染时可以读取到的状态值。你可以把它理解为组件的"当前状态快照"。
    • baseState:这个字段用于在处理更新队列时,作为计算新状态的"基线"。当有多个更新排队时,baseState 确保 React 能够从一个已知且稳定的状态开始计算,避免因为跳过某些更新而导致状态不一致。
    javascript 复制代码
    // ... 在 mountStateImpl 中
    hook.memoizedState = hook.baseState = initialState; // 初始时两者都等于初始值
  • 更新队列(queue):

    • 每个 useState Hook 都会有一个独立的更新队列。这个队列是一个环形链表结构,专门用来存储所有待处理的状态更新。
    • pending: null :指向队列中最新待处理的更新。由于是环形链表,它通常指向链表的尾部。在初始化时,还没有任何更新,所以是 null。
    • lanes: NoLanes :这是一个位掩码(bitmask),用于表示队列中所有更新的优先级集合。NoLanes 表示当前队列中没有任何更新,因此也没有优先级。
    • dispatch: null :这个字段非常关键,它将会在稍后被绑定为我们熟悉的 setState 函数。在初始化阶段,它暂时是 null。
    • lastRenderedReducer: basicStateReducer :这是一个默认的 reducer 函数,用于处理 useState 的状态更新逻辑。它能识别你传入 setState 的是值还是函数。
    • lastRenderedState: initialState :记录上一次渲染时的状态。这个字段与 lastRenderedReducer 一起,用于实现"急切更新"(eager update)优化,在某些情况下可以减少一次不必要的渲染。
    javascript 复制代码
    // ... 在 mountStateImpl 中
    const queue = {
      pending: null, // 指向最新待处理更新(环形链表尾)
      lanes: NoLanes, // 队列中所有更新的优先级集合
      dispatch: null, // 后续绑定的更新触发函数(setXxx)
      lastRenderedReducer: basicStateReducer, // 状态计算函数(默认处理值/函数类型 action)
      lastRenderedState: (initialState: any), // 上一次渲染的状态(用于优化)
    };
    hook.queue = queue;

3.1.4 绑定 dispatch:为什么 setState 引用稳定

你可能注意到,无论组件重新渲染多少次,你从 useState 解构出来的 setState 函数的引用总是稳定的,它不会变。这是 React 内部一个非常巧妙的设计。

  • 闭包与绑定 :
    • 在 mountState 函数中,React 会创建一个 dispatch 函数,并通过 Function.prototype.bind 方法,将当前的 Fiber 节点(currentlyRenderingFiber)和 Hook 的更新队列(queue)"绑定"到 dispatchSetState 函数上。
    • 这个绑定操作确保了 dispatch 函数在组件的整个生命周期中,始终能够正确地找到它所属的 Fiber 节点和更新队列,从而触发正确的状态更新。
javascript 复制代码
function mountState(initialState) {
  const hook = mountStateImpl(initialState);
  const queue = hook.queue;
  // 绑定 dispatch:与当前 Fiber、队列闭包关联,确保稳定性
  const dispatch = dispatchSetState.bind(
    null,
    currentlyRenderingFiber, // 当前正在渲染的 Fiber 节点
    queue // 当前 Hook 的更新队列
  );
  queue.dispatch = dispatch; // 将绑定后的 dispatch 存储到队列中
  return [hook.memoizedState, dispatch]; // 返回 [状态, 更新函数]
}

3.1.5 action 长什么样:值或函数都可以

当你调用 setState(action) 时,action 可以是两种形式:

  1. 直接的值 :例如 setCount(10),此时 action 就是 10。
  2. 一个函数 :例如 setCount(prevCount => prevCount + 1),此时 action 是一个接收上一个状态作为参数并返回新状态的函数。

这两种形式的 action 都是由 basicStateReducer 这个内部函数来处理的:

javascript 复制代码
function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
  return typeof action === "function" ? action(state) : action;
}

3.2 updateState:状态更新流程

当组件已经挂载完成,并且因为 setState 调用导致状态发生变化时,React 会进入"更新"阶段。此时,useState 的内部逻辑会路由到 updateState 函数。

3.2.1 发生时机

  • 组件后续渲染时 :只要组件不是第一次渲染,并且因为某种原因(例如父组件重新渲染、自身状态更新等)需要重新执行函数体时,useState 就会调用 updateState。

3.2.2 useState 是 useReducer 的特殊形式

useState 在底层其实是 useReducer 的一个简化版本。updateState 函数的实现清晰地展示了这一点:

javascript 复制代码
function updateState<S>(
  initialState: (() => S) | S
): [S, Dispatch<BasicStateAction<S>>] {
  // 实际上是调用了 updateReducer,并传入了 basicStateReducer 作为默认的 reducer
  return updateReducer(basicStateReducer, initialState);
}

这意味着 useState 的所有状态更新逻辑,包括如何处理更新队列、如何计算新状态、如何处理优先级等,都复用了 useReducer 的核心机制。useState 只是提供了一个更简洁的 API 封装。

3.2.3 updateReducer 的核心工作

updateReducer 是处理 Hook 状态更新的核心函数。它会遍历 Hook 的更新队列,并根据队列中的更新来计算出最新的状态。

  • 获取 Hook 节点和队列:

    • updateReducer 首先会获取当前正在处理的 Hook 节点(hook)以及它关联的更新队列(queue)。
javascript 复制代码
function updateReducerImpl<S, A>(
  hook: Hook,
  current: Hook, // 上一次渲染的 Hook 节点
  reducer: (S, A) => S
): [S, Dispatch<A>] {
  const queue = hook.queue;
  // ...
}
  • 遍历更新队列,计算新状态:

    • updateReducer 会从 hook.baseState(基线状态)开始,沿着更新队列(queue.pending 指向的环形链表)逐个处理每一个更新。
    • 对于每个更新,它会调用传入的 reducer 函数(对于 useState 来说就是 basicStateReducer),将当前状态和更新的 action 传给 reducer,得到新的状态。
  • 避免不必要的渲染:

    • 当 setState 接收到一个 值 ,并且这个值与当前的 memoizedState (即上一次渲染后的状态) 严格相等 时,React 会判断状态没有实际变化,从而 提前退出 ,不调度一次新的渲染。这是为了避免不必要的计算和 DOM 更新,提高性能。这正是我之前修正的重点。
  • 处理优先级(Lanes):

    • 在遍历更新队列时,updateReducer 还会考虑每个更新的优先级(lane)。它会确保只有那些优先级足够高的更新才会被处理。低优先级的更新可能会被跳过,留待后续的渲染周期处理。

3.2.4 返回值

updateState 最终会返回一个数组 [state, dispatch],其中 state 是经过所有有效更新计算后的最新状态,而 dispatch 仍然是那个稳定的 setState 函数。

3.3 为什么要有 mountState 和 updateState

区分 mountState 和 updateState,是为了在初次渲染时初始化状态与更新队列,在后续更新时复用旧状态、处理待执行更新,既避免初始值重复计算等无效开销,又确保不同阶段状态衔接正确。

职责与执行要点:

  1. mountState(initialState)
  • 创建一个新的 Hook 节点,初始化 memoizedState 为 initialState(若传入函数则调用一次后取其返回值),queue 为空队列;
  • 绑定并返回 dispatch(闭包捕获当前 Fiber 与队列),用于在事件/渲染阶段入队更新;
  • 将 Hook 挂到 workInProgress.memoizedState 的链表中,为后续 Hooks 调用提供遍历基础。
  1. updateState()
  • 读取当前 Hook 的 queue.pending,按优先级(Lanes)应用更新,计算 nextState;
  • 将本次应用后的结果写回 memoizedState 与 lastRenderedState,同步 baseState/baseQueue 以支持未处理的低优先级更新在后续重放;
  • 如果 nextState 与之前值"相等"(用 is 比较),可以触发跳过优化(不产生额外副作用)。

3.4 UpdateQueue 与优先级调度

在 React 中,状态更新并非总是立即执行的。为了保证应用的响应性和性能,React 引入了一套精密的更新队列(UpdateQueue)和优先级调度(Lanes)机制。这套机制确保了即使在短时间内有大量状态更新请求,React 也能以高效且有序的方式处理它们。

3.4.1 UpdateQueue 的数据结构

每个 useState Hook 内部都维护着一个 queue 对象,这个对象就是该 Hook 的更新队列。它是一个环形链表 ,专门用于存储所有待处理的状态更新(Update 对象)。

javascript 复制代码
// Hook 内部的 queue 结构
const queue = {
  pending: null, // 指向队列中最新待处理更新(环形链表尾部)
  lanes: NoLanes, // 队列中所有更新的优先级集合(位掩码)
  dispatch: null, // 绑定后的 setState 函数
  lastRenderedReducer: basicStateReducer, // 状态计算函数
  lastRenderedState: (initialState: any), // 上一次渲染的状态
};
  • pending : 这是一个指向环形链表尾部的指针。由于是环形链表,通过 pending.next 就可以访问到链表的第一个更新。当没有待处理的更新时,pending 为 null。
  • lanes : 这是一个位掩码(bitmask),它聚合了当前 UpdateQueue 中所有 Update 的优先级。React 通过这个 lanes 值来判断当前 Hook 是否有待处理的更新,以及这些更新的最高优先级是什么。
  • dispatch : 这就是我们平时使用的 setState 函数。它在 mountState 阶段被绑定到当前的 Fiber 节点和 queue 上,确保了其引用稳定性。
  • lastRenderedReducer : 对于 useState 而言,它始终是 basicStateReducer,负责处理 action 是值还是函数的情况。
  • lastRenderedState: 记录上一次成功渲染后该 Hook 的状态值。这个值在"急切更新"优化中扮演重要角色。

3.4.2 Update 对象的结构

当调用 setState(action) 时,React 会创建一个 Update 对象,并将其添加到对应的 UpdateQueue 中。一个 Update 对象通常包含以下关键信息:

javascript 复制代码
type Update<S, A> = {
  lane: Lane, // 本次更新的优先级
  action: A, // 更新的动作,可以是新状态值或一个函数
  hasEagerState: boolean, // 是否有急切状态(用于优化)
  eagerState: S | null, // 急切状态值(用于优化)
  next: Update<S, A> | null, // 指向下一个更新
  // ... 其他内部属性
};
  • lane : 表示本次更新的优先级。React 内部使用 Lane(车道)模型来管理优先级,不同的 Lane 代表不同的优先级,例如同步更新、并发更新、离屏更新等。
  • action : 用户传入 setState 的值或函数。
  • hasEagerState 和 eagerState : 用于"急切更新"优化。如果 action 是一个非函数值,并且与当前状态不同,React 会尝试计算出 eagerState 并标记 hasEagerState 为 true。
  • next : 指向 UpdateQueue 中的下一个 Update 对象,构成环形链表。

3.4.3 优先级调度与 Lanes 机制

React 的调度器(Scheduler)会根据 Lanes 来决定何时以及以何种顺序处理更新。

  • Lanes (车道) : React 使用位掩码来表示优先级,每个位代表一个"车道"。数字越小,优先级越高。例如:
    • SyncLane (同步车道): 最高优先级,通常用于用户交互(如点击)。
    • InputContinuousLane (连续输入车道): 较高优先级,用于连续输入事件(如拖拽)。
    • DefaultLane (默认车道): 中等优先级,用于大多数状态更新。
    • IdleLane (空闲车道): 最低优先级,用于不重要的后台任务。
  • 更新的合并与跳过 :
    • 在 updateReducerImpl 中,React 会遍历 UpdateQueue。对于每个 Update,它会检查其 lane 是否在当前渲染的 renderLanes 范围内。
    • 如果 Update 的 lane 不在 renderLanes 范围内(即优先级不够高),这个更新会被跳过 ,并保留在 UpdateQueue 中,等待下一个更高优先级的渲染周期处理。
    • 如果 Update 的 lane 在 renderLanes 范围内,则会被处理,其 action 会被 reducer 函数执行,计算出新的状态。
  • baseState 和 baseQueue :
    • hook.baseState 存储的是上一次成功提交(commit)的状态。
    • hook.baseQueue 存储的是上一次成功提交后,仍然保留在队列中但未被处理的低优先级更新。
    • 在每次更新时,updateReducerImpl 会从 baseState 开始,并首先处理 baseQueue 中的更新,然后处理 pending 队列中的更新。这样可以确保即使有低优先级更新被跳过,它们也能在后续的渲染中被正确处理,保证状态的最终一致性。

3.4.4 批量更新

react 有一道面试题是 setState 是同步还是异步,其实 setState 是否"同步",取决于批量更新与优先级机制。关键点不是"同步/异步"本身,而是它在一次事件循环中如何被收集、何时被计算与提交。

1. 核心机制:自动批量更新
  1. 自动批量与根调度:React 会将同一事件循环/微任务内的多次更新统一合并到根调度队列(ensureRootIsScheduled),并在微任务末尾推进一次工作循环,覆盖事件处理、Promise.then、setTimeout、useEffect 等路径。
  2. 事件优先级与包装:React DOM 事件系统会根据事件类型选择优先级(离散/连续/默认),并在分发时设置当前更新优先级,比如离散事件(如 click、keydown、input 等)会提升到 DiscreteEventPriority。现代并发模式下默认已批量,但还保留unstable_batchedUpdates 用于 legacy 兼容,。
  3. 车道(lanes)与调度:每次 setState 会请求一个更新车道(requestUpdateLane→lane),并把更新入队到 Hook 的并发队列,再调用 scheduleUpdateOnFiber(root, fiber, lane) 合并到根的调度中。

示例:事件内的自动批量更新

javascript 复制代码
function Counter() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState("React");

  const handleClick = () => {
    // React DOM 的事件包装(discreteUpdates)提升为离散事件优先级
    setCount((c) => c + 1);
    setName("Clicked");
    // 同一事件循环/微任务内,以上更新合并到一次根调度与提交
  };

  return (
    <button onClick={handleClick}>
      {name}: {count}
    </button>
  );
}

源码要点:

javascript 复制代码
// 离散事件包装:以更高事件优先级运行(如 click/keydown)
export function discreteUpdates(fn, a, b, c, d) {
  // DOM 事件分发期间提升到 DiscreteEventPriority,确保交互响应
  return discreteUpdatesImpl(fn, a, b, c, d);
}

// Hook 的 setState:创建 Update,急切状态优化,入队并调度
function dispatchSetStateInternal(fiber, queue, action, lane) {
  const update = {
    lane,
    revertLane: NoLane,
    action,
    hasEagerState: false,
    eagerState: null,
    next: null,
  };
  const alternate = fiber.alternate;
  if (
    fiber.lanes === NoLanes &&
    (alternate === null || alternate.lanes === NoLanes)
  ) {
    const lastRenderedReducer = queue.lastRenderedReducer;
    if (lastRenderedReducer !== null) {
      const currentState = queue.lastRenderedState;
      const eagerState = lastRenderedReducer(currentState, action);
      update.hasEagerState = true;
      update.eagerState = eagerState;
      if (is(eagerState, currentState)) {
        // 快路径:状态未变,入队以便后续重基,但当前不触发渲染
        enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update);
        return false;
      }
    }
  }
  // 并发入队,并将更新合并到根的调度
  const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
  if (root !== null) {
    scheduleUpdateOnFiber(root, fiber, lane);
    // 若为过渡车道,做车道关联,保证一致性
    entangleTransitionUpdate(root, queue, lane);
    return true;
  }
  return false;
}

// 并发 Hook 入队(暂存于全局并发队列,稍后一次性落地到各自队列)
export function enqueueConcurrentHookUpdate(fiber, queue, update, lane) {
  const concurrentQueue = queue;
  const concurrentUpdate = update;
  enqueueUpdate(fiber, concurrentQueue, concurrentUpdate, lane);
  return getRootForUpdatedFiber(fiber);
}

// 将更新合并到根并确保根已进入调度;legacy 同步车道立即刷新
export function scheduleUpdateOnFiber(root, fiber, lane) {
  markRootUpdated(root, lane);
  ensureRootIsScheduled(root);
  if (
    lane === SyncLane &&
    executionContext === NoContext &&
    !disableLegacyMode &&
    (fiber.mode & ConcurrentMode) === NoMode
  ) {
    resetRenderTimer();
    flushSyncWorkOnLegacyRootsOnly();
  }
}

// 使用 `didScheduleMicrotask` 标志进行"微任务去重",确保在当前事件循环的微任务阶段,只安排一次统一的调度处理。
export function ensureRootIsScheduled(root: FiberRoot): void {
  if (!didScheduleMicrotask) {
    didScheduleMicrotask = true;
    scheduleImmediateRootScheduleTask();
  }
}

// ------------------------------------
// 并发队列的"处理 update"逻辑(落地到环形链表 + 标记到根)
// ------------------------------------

// 暂存并发更新:将 (fiber, queue, update, lane) 依次压入数组,稍后统一处理
const concurrentQueues = [];
let concurrentQueuesIndex = 0;

function enqueueUpdate(fiber, queue, update, lane) {
  // 暂存到全局并发队列,避免在渲染中直接改动队列
  concurrentQueues[concurrentQueuesIndex++] = fiber;
  concurrentQueues[concurrentQueuesIndex++] = queue;
  concurrentQueues[concurrentQueuesIndex++] = update;
  concurrentQueues[concurrentQueuesIndex++] = lane;

  // 立即在源 fiber 标记 lanes,便于急切退避与后续调度判定
  fiber.lanes = mergeLanes(fiber.lanes, lane);
  const alternate = fiber.alternate;
  if (alternate !== null) {
    alternate.lanes = mergeLanes(alternate.lanes, lane);
  }
}

function finishQueueingConcurrentUpdates() {
  const endIndex = concurrentQueuesIndex;
  concurrentQueuesIndex = 0;
  let i = 0;
  while (i < endIndex) {
    const fiber = concurrentQueues[i];
    concurrentQueues[i++] = null;
    const queue = concurrentQueues[i];
    concurrentQueues[i++] = null;
    const update = concurrentQueues[i];
    concurrentQueues[i++] = null;
    const lane = concurrentQueues[i];
    concurrentQueues[i++] = null;

    // 将 update 追加到 queue.pending 的环形链表
    if (queue !== null && update !== null) {
      const pending = queue.pending;
      if (pending === null) {
        update.next = update; // 首个更新自环
      } else {
        update.next = pending.next; // 插入到首节点之后
        pending.next = update;
      }
      queue.pending = update;
    }

    // 标记 lane 到根路径,确保根被调度
    if (lane !== NoLane) {
      markUpdateLaneFromFiberToRoot(fiber, update, lane);
    }
  }
}

// 渲染阶段消费队列:由 updateReducerImpl 负责(源码摘录)
// 1)把 queue.pending 合并到 baseQueue 的环形链
const pendingQueue = queue.pending;
if (pendingQueue !== null) {
  if (baseQueue !== null) {
    const baseFirst = baseQueue.next;
    const pendingFirst = pendingQueue.next;
    baseQueue.next = pendingFirst;
    pendingQueue.next = baseFirst;
  }
  current.baseQueue = baseQueue = pendingQueue;
  queue.pending = null;
}
// 2)从 baseState 开始遍历环形链表,按优先级与 eagerState 应用更新
const first = baseQueue.next;
let update = first;
let newState = baseState;
do {
  const action = update.hasEagerState ? update.eagerState : update.action;
  newState = reducer(newState, action);
  update = update.next;
} while (update !== null && update !== first);
hook.memoizedState = newState;
queue.lastRenderedState = newState;

触发更新完整源码流程:\

2. 同步更新(特殊情况):ReactDOM.flushSync(当前行为)

尽管 React 默认进行自动批量更新,但在某些特殊场景下,你可能需要强制同步并立即刷新 DOM,这时可以使用 ReactDOM.flushSync。

flushSync 会在一个受控的 Batched 上下文里,提升当前更新优先级为 DiscreteEventPriority,并在不处于渲染/提交上下文时,统一同步 flush 所有根的同步工作。

示例:使用 ReactDOM.flushSync 强制同步更新

javascript 复制代码
import { flushSync } from "react-dom";

function MyComponent() {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    flushSync(() => {
      setCount((c) => c + 1); // 立即更新并同步提交
    });
    console.log("Count after flushSync:", count);
  };

  return <button onClick={handleClick}>{count}</button>;
}

注意事项:

  • 若在渲染或提交上下文内调用,会被阻止并在开发模式下给出警告;请改为在调度任务或微任务中调用。
  • 频繁使用 flushSync 会破坏并发与批处理的收益,导致性能下降;仅在确实需要同步可见性的场景使用。
  • 受控组件的事件尾恢复在 DOM 层通过 finishEventHandler 触发,会在必要时执行一次同步 flush 以保证受控值一致。
3.结论

setState 并非固定"同步或异步",React 默认自动批量与并发,事件内多次更新仅入队合并,渲染/提交统一在之后进行(语义上偏异步)。只有在"非批上下文且为同步车道"或显式调用 flushSync 时,会同步刷新并立即提交(呈现为同步)。

3.4.5 状态预计算 (Eager State Computation)

在 dispatchSetStateInternal 函数中,React 会尝试进行"急切状态计算"优化。

  • 优化原理 : 如果 action 是一个非函数值,并且当前 Hook 的 queue.lastRenderedReducer 是 basicStateReducer,React 会尝试立即计算出新的状态 eagerState。
  • 提前退出 : 如果计算出的 eagerState 与 queue.lastRenderedState 相同,并且没有其他高优先级的更新,React 就可以提前退出,避免调度一次不必要的渲染。这大大减少了不必要的计算和渲染开销。
  • 条件 : 这种优化只在满足特定条件时发生,例如 action 必须是值而不是函数,并且没有其他待处理的更新会影响最终状态。
javascript 复制代码
const currentState: S = (queue.lastRenderedState: any);
const eagerState = lastRenderedReducer(currentState, action);
// 这里的 `is(eagerState, currentState)` 用于比较新旧状态是否相等
// 如果相等,则通过 `return false` 阻止后续的调度。
if (is(eagerState, currentState)) {
  enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update);
  return false; // 提前退出,不调度新的渲染
}

四、useEffect 的底层实现机制

useEffect 是 React Hooks 中一个非常重要的 Hook,它允许你在函数组件中执行副作用(side effects),例如数据获取、订阅或手动更改 DOM。理解 useEffect 的底层机制对于深入掌握 React 的渲染流程和性能优化至关重要。

4.1 Effect 链表结构

与 useState 类似,useEffect 也是通过链表的形式存储在 Fiber 节点上的。每个 useEffect Hook 都会在对应的 Fiber 节点上创建一个 Effect 对象,这些 Effect 对象通过 next 指针连接起来,形成一个 Effect 链表。这个链表最终会挂载到 Fiber 节点的 updateQueue 属性上。

每个 Effect 对象通常包含以下关键属性:

  • tag : 一个位掩码,用于标识 Effect 的类型和特性,例如 HookHasEffect(表示有副作用需要执行)、HookLayout(对应 useLayoutEffect)、HookPassive(对应 useEffect)。
  • create : useEffect 回调函数本身,即你传入 useEffect 的第一个参数。它会在副作用执行时被调用。
  • destroy : create 函数的返回值,通常是一个清理函数。它会在组件卸载或 Effect 重新执行前被调用,用于清理副作用(例如取消订阅、清除定时器)。
  • deps : 依赖项数组,即你传入 useEffect 的第二个参数。React 会根据这个数组来判断 Effect 是否需要重新执行。
  • next : 指向 Effect 链表中的下一个 Effect 对象。

简易结构示意:

javascript 复制代码
// 假设 FiberNode 上有一个 updateQueue 属性,其中包含 lastEffect 指针
// FiberNode.updateQueue.lastEffect -> Effect1 -> Effect2 -> Effect3 -> Effect1 (循环链表)

interface Effect {
  tag: number; // HookFlags,例如 HookPassive
  create: () => (() => void) | void; // 副作用函数
  destroy: (() => void) | void; // 清理函数
  deps: Array<any> | null; // 依赖项数组
  next: Effect; // 指向下一个 Effect
}

interface FunctionComponentUpdateQueue {
  lastEffect: Effect | null;
  // ... 其他属性,如 events, stores, memoCache
}

interface FiberNode {
  // ... 其他 Fiber 属性
  updateQueue: FunctionComponentUpdateQueue | null;
  // ...
}

4.2 EffectInstance 与生命周期管理

useEffect 的生命周期管理主要围绕着 create 和 destroy 函数的执行时机。React 会在不同的阶段调用这些函数,以确保副作用的正确执行和清理。

4.2.1 整体时序与执行点

无论是首次渲染还是更新,React 都会遵循 "触发 → 渲染 → 提交 → 绘制 → 被动处理" 的核心流程,各阶段职责清晰且顺序固定:

  1. 触发阶段由事件处理(如点击)、setState、dispatch、startTransition、useEffect 异步回调等触发,进入调度流程。
  2. 渲染阶段(Render)构建/复用 Fiber、diff 得出宿主层变更与 Hooks 的 Effect 列表;不会触碰 DOM。
  3. 提交阶段(Commit)(不可中断,确保 DOM 操作原子性) 变更前(Before Mutation):焦点、视图过渡等前置处理。 变更(Mutation):应用 DOM 插入/重排/删除;执行 useInsertionEffect 的清理与安装;执行 useLayoutEffect 的清理。 布局(Layout):同步执行类组件 DidMount/DidUpdate 、 ref 绑定、以及 useLayoutEffect 的安装(effect 本体)。
  4. 绘制阶段(Paint):浏览器将上述结果绘制到屏幕。
  5. 被动阶段(Passive)完成可见绘制后,在一个独立宏任务中"冲洗" useEffect (即 HookPassive )的清理与安装,避免阻塞提交与布局。

commitHookEffectListUnmount / commitHookEffectListMount 专门处理 useEffect 的副作用(HookPassive 类型),执行时机完全落在 "被动阶段",调用链和作用如下:

  1. commitHookEffectListUnmount(useEffect 的清理函数执行) 执行阶段:被动阶段(浏览器绘制之后,异步执行)。 核心作用:遍历标记为 HookPassive 且 HookHasEffect 的 Effect 链表,执行 effect.destroy 清理函数(如移除事件监听、清除定时器)。

  2. commitHookEffectListMount 执行阶段:被动阶段(commitHookEffectListUnmount 之后,仍在同一宏任务中)。 核心作用:遍历相同标记的 Effect 链表,执行 effect.create 回调函数(如发起网络请求、订阅事件),并将回调返回的清理函数存入 effect.destroy,供下一次清理使用。

useLayoutEffect 与 useEffect 不同的地方就是 useLayoutEffect 清理在变更(Mutation)阶段,安装在布局(Layout)阶段,同步发生、绘制前执行,适合同步读/写布局(测量尺寸、直接操作 DOM)。

4.2.2 挂载阶段 (Mount Phase)

当组件首次渲染并挂载到 DOM 后,React 会执行 useEffect 的 create 函数。这个过程主要由 commitHookEffectListMount 函数负责。

commitHookEffectListMount 核心逻辑:

javascript 复制代码
// 简化后的 commitHookEffectListMount 示例
function commitHookEffectListMount(flags, finishedWork) {
  const updateQueue = finishedWork.updateQueue;
  const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;

  if (lastEffect !== null) {
    const firstEffect = lastEffect.next;
    let effect = firstEffect;
    do {
      // 检查 Effect 的 tag 是否包含指定的 flags (例如 HookPassive)
      if ((effect.tag & flags) === flags) {
        const create = effect.create;
        // 执行 create 函数,并将其返回值(清理函数)存储到 effect.destroy 上
        effect.destroy = create();
      }
      effect = effect.next;
    } while (effect !== firstEffect);
  }
}
  • commitHookEffectListMount 会遍历 Fiber 节点上的 Effect 链表。
  • 对于符合条件的 Effect(例如 useEffect 对应的 HookPassive 类型的 Effect),它会调用 effect.create() 函数。
  • create 函数的返回值(如果存在)会被保存到 effect.destroy 属性上,作为后续清理的依据。

4.2.3 卸载阶段 (Unmount Phase)

当组件从 DOM 中卸载时,React 会执行 useEffect 的 destroy 函数,以清理之前创建的副作用。这个过程主要由 commitHookEffectListUnmount 函数负责。

commitHookEffectListUnmount 核心逻辑:

javascript 复制代码
// 简化后的 commitHookEffectListUnmount 示例
function commitHookEffectListUnmount(flags, finishedWork) {
  const updateQueue = finishedWork.updateQueue;
  const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;

  if (lastEffect !== null) {
    const firstEffect = lastEffect.next;
    let effect = firstEffect;
    do {
      // 检查 Effect 的 tag 是否包含指定的 flags
      if ((effect.tag & flags) === flags) {
        const destroy = effect.destroy;
        // 如果存在清理函数,则执行它
        if (destroy !== undefined && destroy !== null) {
          destroy();
        }
      }
      effect = effect.next;
    } while (effect !== firstEffect);
  }
}
  • commitHookEffectListUnmount 同样会遍历 Effect 链表。
  • 对于符合条件的 Effect,它会检查 effect.destroy 是否存在,如果存在,则执行该清理函数。

4.2.4 更新阶段 (Update Phase) 与延迟执行

在组件更新时,如果 useEffect 的依赖项发生了变化,React 会先执行上一次 Effect 的 destroy 函数,然后再执行新的 create 函数。值得注意的是,useEffect(即 HookPassive 类型的 Effect)的 create 和 destroy 函数的执行是延迟的 ,它们不会在同步的 commit 阶段立即执行,而是会在浏览器完成绘制之后,在一个单独的异步任务中执行。这个延迟执行的机制主要由 flushPassiveEffects 函数及其相关的调度器(Scheduler)负责。

flushPassiveEffects 的作用:

  • flushPassiveEffects 是一个在 commit 阶段之后被调用的函数,它负责收集所有待执行的 HookPassive 类型的 Effect。
  • 它会将这些 Effect 调度到一个低优先级的任务中,等待浏览器空闲时执行。
  • 这种延迟执行的策略可以避免阻塞主线程,提高用户体验,确保动画和用户交互的流畅性。

4.3 依赖比较与副作用调度

useEffect 的核心在于它能够根据依赖项的变化来决定是否重新执行副作用。这就像给 React 装上了一双"火眼金睛",能够精准地识别出哪些"任务"需要重新执行,哪些可以"偷懒"跳过。而副作用的调度,则像一位"幕后英雄",默默地在合适的时机执行这些任务,确保应用的性能和用户体验。

4.3.1 依赖项比较

在每次组件渲染时,useEffect 都会拿到新的依赖项数组 nextDeps,并与上一次渲染的依赖项数组 prevDeps 进行比较。这个比较工作主要由 areHookInputsEqual 函数完成。

javascript 复制代码
function areHookInputsEqual(
  nextDeps: Array<mixed>,
  prevDeps: Array<mixed> | null
): boolean {
  if (prevDeps === null) {
    // 首次渲染或依赖项从无到有,总是返回 false,意味着副作用总是会执行。
    return false;
  }

  // 核心比较逻辑:遍历依赖数组,使用 Object.is 进行浅比较
  for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
    if (is(nextDeps[i], prevDeps[i])) {
      continue; // 如果当前依赖项相同,则继续比较下一个
    }
    return false; // 发现不同,立即返回 false
  }
  return true; // 所有依赖项都相同,返回 true
}
4.3.2 副作用的调度

当组件首次挂载时,mountEffectImpl 函数会被调用。它的主要职责是:

javascript 复制代码
function mountEffectImpl(
  fiberFlags: Flags,
  hookFlags: HookFlags,
  create: () => (() => void) | void, // 副作用函数
  createDeps: Array<mixed> | void | null // 依赖项
  // ... 其他参数
): void {
  const hook = mountWorkInProgressHook(); // 创建新的 Hook 节点
  const nextDeps = createDeps === undefined ? null : createDeps;
  currentlyRenderingFiber.flags |= fiberFlags; // 标记 Fiber 节点需要执行副作用
  hook.memoizedState = pushSimpleEffect(
    // 将副作用信息存储到 Hook 节点的 memoizedState 中
    HookHasEffect | hookFlags,
    createEffectInstance(),
    create,
    nextDeps
  );
}
  • 创建 Hook 节点 :mountWorkInProgressHook() 会创建一个新的 Hook 节点,用于存储 useEffect 的相关信息。
  • 标记 Fiber 节点 :currentlyRenderingFiber.flags |= fiberFlags; 会给当前的 Fiber 节点打上一个 fiberFlags 标记(例如 PassiveEffect),这个标记告诉 React 在提交阶段需要处理这个副作用。
  • 存储副作用信息 :pushSimpleEffect 会将副作用函数 create、依赖项 nextDeps 以及其他相关信息封装成一个 Effect 对象,存储再 fiber 的 updateQueue 中,并返回这个对象,后再存储在 Hook 节点的 memoizedState 中。
4.3.2.2 更新阶段

当组件更新时,updateEffectImpl 函数会被调用。这是 useEffect 依赖项比较和副作用调度最关键的地方:

javascript 复制代码
function updateEffectImpl(
  fiberFlags: Flags,
  hookFlags: HookFlags,
  create: () => (() => void) | void, // 副作用函数
  deps: Array<mixed> | void | null // 依赖项
): void {
  const hook = updateWorkInProgressHook(); // 获取当前 Hook 节点
  const nextDeps = deps === undefined ? null : deps;
  const effect: Effect = hook.memoizedState; // 获取上一次渲染的副作用信息
  const inst = effect.inst;

  if (currentHook !== null) {
    if (nextDeps !== null) {
      const prevEffect: Effect = currentHook.memoizedState;
      const prevDeps = prevEffect.deps;
      // 核心:调用 areHookInputsEqual 比较依赖项
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        // 如果依赖项没有变化,则跳过副作用的重新执行
        hook.memoizedState = pushSimpleEffect(
          hookFlags,
          inst,
          create,
          nextDeps
        );
        return; // 直接返回,不设置 Fiber 节点的 flags
      }
    }
  }

  // 如果依赖项发生变化,或者 prevDeps 为 null (不应该发生在这里,因为是更新阶段)
  // 则标记 Fiber 节点需要执行副作用
  currentlyRenderingFiber.flags |= fiberFlags;

  hook.memoizedState = pushSimpleEffect(
    HookHasEffect | hookFlags,
    inst,
    create,
    nextDeps
  );
}
4.3.3 副作用的调度时机

useEffect 的副作用(默认情况下)是在浏览器完成绘制之后异步执行的。这通过 PassiveEffect 标志位来实现。

  • 当 useEffect 的依赖项发生变化时,updateEffectImpl 会给当前的 Fiber 节点打上 PassiveEffect 标记。
  • 在 React 的提交阶段,它会遍历所有带有 PassiveEffect 标记的 Fiber 节点,并执行它们对应的副作用函数。
  • 由于这些副作用是在浏览器绘制之后执行的,它们不会阻塞用户界面的渲染,从而保证了应用的响应性。

五 一些实践优化建议

通过对 useState 和 useEffect 底层源码的深入剖析,我们可以提炼出一些指导我们日常开发的最佳实践。这些实践并非空穴来风,而是基于 React 内部机制的优化考量。

5.1 useState

  1. 懒初始化(Lazy Initialization):

    • 源码 :在 mountStateImpl 中,如果 useState 的初始值是一个函数,React 只会在首次渲染时执行它一次。
    • 实践建议 :当初始状态的计算成本较高(例如需要进行大量计算或数据转换)时,应传入一个函数作为 useState 的初始值,而不是直接传入计算结果。这可以避免在每次组件渲染时都执行不必要的计算。
    javascript 复制代码
    // 避免:每次渲染都执行 expensiveCalculation
    const [data, setData] = useState(expensiveCalculation());
    
    // 推荐:只在首次渲染时执行 expensiveCalculation
    const [data, setData] = useState(() => expensiveCalculation());
  2. 状态的不可变性(Immutability):

    • 源码 :updateReducer 在处理更新时,会比较新旧状态是否严格相等(Object.is)。如果相等,React 会跳过后续的渲染。
    • 实践建议:永远不要直接修改状态对象或数组。当你需要更新对象或数组状态时,应该创建新的对象或数组,并用新值替换旧值。这能确保 React 正确检测到状态变化,并触发必要的重新渲染。
    javascript 复制代码
    // 避免:直接修改对象
    const [user, setUser] = useState({ name: "Alice", age: 30 });
    user.age = 31; // ❌ React 可能不会重新渲染
    
    // 推荐:创建新对象
    setUser((prevUser) => ({ ...prevUser, age: 31 })); // ✅
  3. 批量更新(Batching Updates):

    • 源码 :React 内部会批量处理在同一事件循环中触发的多个 setState 调用,以减少不必要的重新渲染次数。
    • 实践建议 :在事件处理函数或异步操作中,即使多次调用 setState,通常也只会触发一次重新渲染。了解这一点可以帮助你避免过度优化,并相信 React 的性能机制。

5.2 useEffect

  1. 精确的依赖项(Precise Dependencies):

    • 源码 :areHookInputsEqual 函数通过浅比较(Object.is)来判断依赖项是否发生变化。
    • 实践建议 :
      • 不要遗漏依赖项 :确保 useEffect 的依赖数组包含了所有在副作用函数内部使用的、且在组件渲染过程中可能发生变化的值(props、state、函数等)。遗漏依赖项会导致副作用在应该重新执行时却不执行,从而引入 bug。
      • 避免不必要的依赖项 :如果某个值在副作用函数内部使用,但它在组件的整个生命周期中都不会改变(例如常量、外部函数),则可以将其从依赖数组中移除,或者使用 useCallback/useMemo 进行记忆化,以避免不必要的副作用重新执行。
      • 对象和数组的引用问题 :由于 areHookInputsEqual 进行的是浅比较,如果依赖项中包含对象或数组,即使其内部属性发生变化,只要引用不变,useEffect 就不会重新执行。此时,你需要确保每次更新都生成新的对象/数组引用,或者使用 useMemo 来记忆化这些对象/数组。
  2. 恰当的清理函数(Proper Cleanup):

    • 源码 :useEffect 的副作用函数可以返回一个清理函数,这个函数会在组件卸载或副作用重新执行前被调用。
    • 实践建议:对于任何需要订阅、定时器、网络请求等可能导致内存泄漏或资源占用的副作用,务必提供一个清理函数。这能确保在组件生命周期结束或副作用重新执行时,及时释放资源,避免不必要的性能开销和 bug。
    javascript 复制代码
    useEffect(() => {
      const timer = setInterval(() => {
        console.log("Tick");
      }, 1000);
    
      return () => {
        clearInterval(timer); // 清理定时器
      };
    }, []);
  3. 理解执行时机(Understanding Execution Timing):

    • 源码 :PassiveEffect 标志位决定了 useEffect 的副作用是在浏览器绘制之后异步执行的。
    • 实践建议 :useEffect 适用于那些不影响 DOM 布局或渲染的副作用,例如数据获取、订阅、日志记录等。如果你的副作用需要同步修改 DOM 并并在浏览器绘制前完成,你应该考虑使用 useLayoutEffect。

六. 最后

React Hooks 的设计精妙之处在于其对 Fiber 架构的充分利用,通过 Hook 链表和更新队列,实现了函数组件状态和副作用的有效管理。理解这些底层机制,不仅能帮助我们更深入地掌握 React 的工作原理,也能在日常开发中更好地优化组件性能,避免潜在的问题。

相关推荐
java_nnnn5 小时前
JavaEE进阶-HTML基础认识
前端·java-ee·html
鬼手点金5 小时前
Claude Code示范案例-常用快捷命令
java·服务器·前端·计算机视觉·前向传播
CopyCode5 小时前
我排查了一下午,发现项目打包体积翻倍的元凶是它
前端·性能优化
u0111026756 小时前
Vue 3 实现图片裁剪框:拖动、缩放与固定宽高比
前端·javascript·vue.js
汉堡大王95276 小时前
一张图三句需求,我用 Trae Work 做了一块能看日出日落和月相的天文机械表
前端·后端·github
小小善后师6 小时前
用 Canvas + AI 实现登录页的 Logo 粒子动画
前端·vue.js
Amos_Web6 小时前
Rspack 源码解析(十六):多类型资源如何进入 Compilation.assets
前端·rust·前端框架
GAMC6 小时前
chrome-devtools-mcp:让 AI 编码助手真正"看见"浏览器
前端·人工智能
deli0076 小时前
高尔顿板:把球一颗颗丢下去,为什么最后总堆成一座钟形山
前端
java_nnnn6 小时前
JavaEE进阶-CSS初识
java·前端·css·java-ee·html