深入 useState、useEffect 的底层实现

一、前言

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

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

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

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

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

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

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

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

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

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

它们如何协同工作?

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

理解这三者之间的关系,是理解 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 useStateuseReducer 的特殊形式

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 节点,初始化 memoizedStateinitialState(若传入函数则调用一次后取其返回值),queue 为空队列;
  • 绑定并返回 dispatch(闭包捕获当前 Fiber 与队列),用于在事件/渲染阶段入队更新;
  • Hook 挂到 workInProgress.memoizedState 的链表中,为后续 Hooks 调用提供遍历基础。
  1. updateState()
  • 读取当前 Hook 的 queue.pending,按优先级(Lanes)应用更新,计算 nextState
  • 将本次应用后的结果写回 memoizedStatelastRenderedState,同步 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 就可以访问到链表的第一个更新。当没有待处理的更新时,pendingnull
  • 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 的值或函数。
  • hasEagerStateeagerState : 用于"急切更新"优化。如果 action 是一个非函数值,并且与当前状态不同,React 会尝试计算出 eagerState 并标记 hasEagerStatetrue
  • next : 指向 UpdateQueue 中的下一个 Update 对象,构成环形链表。

3.4.3 优先级调度与 Lanes 机制

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

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

3.4.4 批量更新

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

1. 核心机制:自动批量更新
  1. 自动批量与根调度:React 会将同一事件循环/微任务内的多次更新统一合并到根调度队列(ensureRootIsScheduled),并在微任务末尾推进一次工作循环,覆盖事件处理、Promise.thensetTimeoutuseEffect 等路径。
  2. 事件优先级与包装:React DOM 事件系统会根据事件类型选择优先级(离散/连续/默认),并在分发时设置当前更新优先级,比如离散事件(如 clickkeydowninput 等)会提升到 DiscreteEventPriority。现代并发模式下默认已批量,但还保留unstable_batchedUpdates 用于 legacy 兼容,。
  3. 车道(lanes)与调度:每次 setState 会请求一个更新车道(requestUpdateLanelane),并把更新入队到 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.lastRenderedReducerbasicStateReducer,React 会尝试立即计算出新的状态 eagerState
  • 提前退出 : 如果计算出的 eagerStatequeue.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 的生命周期管理主要围绕着 createdestroy 函数的执行时机。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 会执行 useEffectcreate 函数。这个过程主要由 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 会执行 useEffectdestroy 函数,以清理之前创建的副作用。这个过程主要由 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)的 createdestroy 函数的执行是延迟的 ,它们不会在同步的 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 节点,并执行它们对应的副作用函数。
  • 由于这些副作用是在浏览器绘制之后执行的,它们不会阻塞用户界面的渲染,从而保证了应用的响应性。

五 一些实践优化建议

通过对 useStateuseEffect 底层源码的深入剖析,我们可以提炼出一些指导我们日常开发的最佳实践。这些实践并非空穴来风,而是基于 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 的工作原理,也能在日常开发中更好地优化组件性能,避免潜在的问题。

相关推荐
Tzarevich8 小时前
React 中的 JSX 与组件化开发:以函数为单位构建现代前端应用
前端·react.js·面试
李香兰lxl8 小时前
A I时代如何在研发团队中展现「前端」的魅力
前端
本末倒置1838 小时前
解决 vue2.7使用 pnpm 和 pinia 2.x报错
前端
CoderLiz8 小时前
Flutter中App升级实现
前端
别急国王8 小时前
React Hooks 为什么不能写在判断里
react.js
Mintopia8 小时前
⚛️ React 17 vs React 18:Lanes 是同一个模型,但跑法不一样
前端·react.js·架构
李子烨8 小时前
吃饱了撑的突发奇想:TypeScript 类型能不能作为跑业务逻辑的依据?(纯娱乐)
前端·typescript
AAA简单玩转程序设计8 小时前
救命!Java小知识点,基础党吃透直接起飞
java·前端
叫我詹躲躲8 小时前
Vue 3 动态组件详解
前端·vue.js