React状态管理的实现原理: 从hook到渲染

这篇文章主要是一起了解一下React的useState是如何工作的

cover

React状态管理的实现原理:从Hook到渲染

一个看似简单的更新背后

在React开发中,我们经常会写这样的代码:

function Counter() {
  const [count, setCount] = useState(0);
  const [step, setStep] = useState(1);

  return (
    <button onClick={() => {
      setCount(count + 1);
      setStep(step + 1);
    }}>
      当前计数 {count}
      当前步长 {step}
    </button>
  );
}

这个简单的计数器组件看似平常,但它却涉及了React状态管理的核心机制。当我们点击按钮时,React是如何处理这两个状态更新的?它们是立即更新还是批量处理?更新后,组件是如何重新渲染的? 让我们深入React的源码实现,揭开这些问题的答案

一、Hook的内部结构:状态管理的基石

1. Hook链表:状态的存储方式

hook链表示意图

image.png image.png

首先,React需要一种方式来保存组件的多个状态。它使用了一个精心设计的链表结构:

interface Hook {
    memorizedState: any;    // 保存状态值
    updateQueue: unknown;    // 更新队列
    next: Hook | null;      // 指向下一个Hook
}

这个看似简单的结构实际上是React状态管理的核心。让我们来解析它的设计思想:

  • memorizedState: 用于存储当前Hook的状态。对于useState,它存储状态值;对于useEffect,它存储effect的相关信息。
  • updateQueue: 存储更新队列,实现批量更新和优先级管理。
  • next: 链表指针,连接同一组件内的多个Hook。

2. Hook的创建过程

当组件首次渲染时,每个Hook都会被创建并加入链表,以useState为例:

// 【首次渲染的 useState】
function mountState<State>(initialState: State): [State, Dispatch<State>] {
    // 1. 创建新的Hook节点,挂载到Fiber的memorizedState上
    // 2. 其余hook依次挂在最后一个hook的next下
    const hook = mountWorkInProgressHook();

    // 2. 保存初始状态
    hook.memorizedState = typeof initialState === 'function'
        ? initialState()
        : initialState;

    // 3. 创建更新队列
    const queue = createUpdateQueue<State>();
    hook.updateQueue = queue;

    // 4. 创建dispatch函数
    const dispatch = dispatchSetState.bind(
        null,
        currentlyRenderingFiber,
        queue
    );

    return [hook.memorizedState, dispatch];
}

这些Hook节点会按照调用顺序连接成一个链表,保存在组件对应的Fiber节点上。

二、状态更新机制:从调用到调度

1. 触发更新

当我们调用setCount(count + 1)时,实际上调用的是dispatchSetState:

function dispatchSetState<State>(
    fiber: FiberNode,
    updateQueue: UpdateQueue<State>,
    // count + 1 就是action,action也可以是函数
    action: Action<State>
) {
    // 1. 获取本次更新的优先级
    const lane = requestUpdateLane();

    // 2. 创建更新对象
    const update = createUpdate(action, lane);

    // 3. 将更新加入队列
    enqueueUpdate(updateQueue, update);

    // 4. 调度更新
    scheduleUpdateOnFiber(fiber, lane);
}

这个过程不会立即更新状态,而是将更新放入队列中,这就是为什么我们可以在一个事件处理函数中多次调用setState而只会触发一次渲染。

2. 处理更新

更新链式队列示意图

image.png image.png

当React开始处理这些更新时(注意useState的触发时机是FC组件重新被调用,下文提到的 renderWithHooks 会描述调用FC的过程):

// 【首次渲染之后实际调用的 useState】
function updateState<State>(): [State, Dispatch<State>] {
    // 1. 会从currentlyRenderingFiber中取alternate(当前页面使用的fiber) 取hook复用
    // 2. currentlyRenderingFiber 是全局变量,会在调用Function组件之前被赋值【见renderWithHooks】
    const hook = updateWorkInProgressHook();
    const queue = hook.updateQueue as UpdateQueue<State>;

    // 获取并清空pending队列
    const pending = queue.shared.pending;
    queue.shared.pending = null;

    if (pending !== null) {
        // 计算最新的状态
        const { memorizedState } = processUpdateQueue(
            hook.memorizedState,
            pending,
            renderLane
        );
        hook.memorizedState = memorizedState;
    }

    return [hook.memorizedState, queue.dispatch];
}

React会批量处理队列中的所有更新,计算出最终的状态值。

三、从状态更新到页面渲染

1. 调度阶段

更新被调度后,React会启动一个完整的渲染流程:

function scheduleUpdateOnFiber(fiber: FiberNode, lane: Lane) {
    // 找到根节点
    const root = markUpdateLaneFromFiberToRoot(fiber, lane);
    // 请求调度
    ensureRootIsScheduled(root);
}

这个过程涉及React的几个核心概念:

  • Fiber架构: 支持增量渲染和优先级调度
  • Scheduler: 决定何时执行更新
  • Lane模型: 精细的优先级控制

2. 协调阶段

从触发更新的组件开始,React会:

1. 构建workInProgress树

export function renderWithHooks(workInProgress: FiberNode, lane: Lane) {
    currentlyRenderingFiber = workInProgress;
    // 重置Hook链表,准备新的渲染
    workInProgress.memorizedState = null;
    workInProgress.updateQueue = null;

    // 执行组件渲染
    const Component = workInProgress.type;

    // 这里的 Component 就是react项目中见到的FC 也就是一开始说的 Counter 组件
    const children = Component(props);

    return children;
}

随着 Component的调用,则前文的 updateState也会被调用,自然会触发processUpdateQueue消费更新链式队列,计算出最新的状态,也就是useState的state值。

2. Diff算法 renderWithHooks返回的新children会和之前的fiber节点进行diff

3. 提交阶段(Commit)

这是实际更新DOM的阶段,涉及:

  1. 双缓存机制
    • workInProgress树完成后切换为current树
    • 最小化DOM操作,提升性能
    • ‘归’阶段构建dom,最终会形成一个离屏dom,所以只会触发一次页面重新渲染
  2. 副作用处理
    • 执行useEffect/useLayoutEffect
    • 处理DOM更新
    • 调用生命周期方法 …

整个过程中,React还实现了许多优化机制:

  1. 批量更新:合并多次setState
  2. Suspense:优雅处理异步加载
  3. 并发特性:可中断的渲染过程
  4. 自动批处理:React 18的新特性

这些机制共同构成了React的”协调引擎”,使得:

  1. 状态更新可控
  2. 渲染过程可中断
  3. 用户交互得到及时响应
  4. DOM操作高效执行

结语:设计思想的启示

React的状态管理实现展现了几个关键的设计思想:

  1. 数据结构的选择
  1. 更新机制的设计

这些设计不仅解决了状态管理的具体问题,更展示了如何构建一个灵活且可维护的系统。通过深入理解这些实现细节,我们不仅能更好地使用React,也能学习到优秀的系统设计思想。

这就是React状态管理的优雅之处:简单的API背后是深思熟虑的设计,每一个选择都在服务于更好的开发体验和系统可维护性。