React 函数组件是如何触发更新的

React 函数组件自从 Hooks 的引入以来,成为现代 React 应用开发的核心。相比类组件,函数组件不仅更简洁,还拥有更强大的功能。不过,理解它背后的更新机制,尤其是从源码层面的视角,能帮助我们更好地优化性能并避免不必要的重新渲染。本文将从 React 的底层源码出发,深度解析函数组件的更新机制。

1. 函数组件更新的触发条件

函数组件的更新主要通过以下几种方式触发:

  • State 变化:通过 useState 的 setter 方法更新状态。
  • Props 变化:当父组件传递的 props 发生变化时,React 会重新渲染该组件。
  • Context 变化:使用 useContext 获取的上下文数据发生变化时,React 会重新渲染相关组件。

从源码的角度看,React 会将组件的状态、props 等数据存储在内部的一个 Fiber 树 中,当这些数据发生变化时,React 会进入调和(Reconciliation)阶段,决定是否需要重新渲染组件。

2. Fiber 树的核心作用

在 React 16 引入 Fiber 架构之后,所有组件(包括类组件和函数组件)的更新都被表示为一个 Fiber 节点。Fiber 树的核心作用是将更新过程分为多个小任务来执行,而不是一次性完成,这样 React 可以在渲染大任务时保持对用户界面的响应性。

每个函数组件都会拥有一个对应的 Fiber 对象,该对象记录了组件的 状态(state)props 以及 更新队列(update queue)。当调用 setStateuseState 的 setter 函数时,React 会将这个更新存入 Fiber 节点的更新队列中,等待调度器来执行。

3. React 调度更新的过程

当函数组件的状态或 props 发生变化时,React 会进入更新调度过程。核心流程如下:

3.1 setState 及 useState 的更新机制

useState 中的 setter 方法(如 setState)被调用时,实际上会创建一个 更新对象,该对象包含了新的状态值以及需要更新的组件引用。这个更新对象会被加入当前 Fiber 节点的更新队列中,等待 React 调度。

1
// 简化后的 useState 实现
2
function useState(initialState) {
3
const hook = getHook(); // 从当前 Fiber 节点获取 hook 状态
4
if (!hook) {
5
// 初始化 hook 状态
6
return mountState(initialState);
7
}
8
return updateState(hook);
9
}

每次更新,React 会根据 Fiber 树中的每个 Fiber 节点执行更新逻辑。它通过 beginWork 函数检查更新队列,重新计算状态值,并触发组件的重新渲染。

3.2 Hooks 的存储与重用

在函数组件的每次执行过程中,React 会通过一个内部链表来保存和重用 Hooks。每个 useStateuseEffect 调用都会在这条链表上创建或复用一个 hook 节点,从而存储状态值或副作用。

1
function renderWithHooks(currentFiber, nextChildren) {
2
currentlyRenderingFiber = currentFiber;
3
currentHook = currentFiber.memoizedState; // 取出之前存储的 hook 链表
4
nextChildren = Component(props); // 重新执行函数组件
5
return nextChildren;
6
}

每次函数组件更新时,React 会重头开始执行函数体,但每个 Hook 都是按照顺序保存的,因此可以依次取出对应的状态和副作用,保持一致性。

4. 调和算法与虚拟 DOM 的工作原理

4.1 虚拟 DOM 的 Diff 算法

React 的更新机制依赖于 调和算法(Reconciliation) 来决定哪些部分需要更新。调和的核心步骤包括:

  1. 创建新的虚拟 DOM:每次组件更新时,React 会根据新的状态和 props 生成一棵新的虚拟 DOM 树。
  2. Diffing 阶段:React 比较新旧两棵虚拟 DOM 树,通过 Diff 算法找出需要修改的部分。
  3. 更新实际 DOM:React 将差异最小化后,批量更新实际的 DOM。

Fiber 树使 React 可以在工作单元(更新步骤)之间暂停和恢复,优化了长任务的执行过程,提高了应用的响应性。

4.2 更新优先级与调度

React 使用 优先级队列 来控制更新的调度顺序。每次状态或 props 变化时,React 会将更新任务赋予一个优先级,根据任务的紧急程度决定何时执行。

  • 高优先级更新(如用户输入事件)会立即执行。
  • 低优先级更新(如动画或后台数据加载)则会延后执行,保证 UI 的流畅性。

5. 函数组件性能优化:useMemo 与 useCallback

由于每次组件更新都会重新执行整个函数,因此使用 useMemouseCallback 来缓存一些昂贵的计算或函数引用非常关键。

  • useMemo:用于缓存计算结果,避免在每次渲染时重复计算。
  • useCallback:用于缓存函数,避免在子组件中每次都创建新的函数引用。
1
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
2
3
const memoizedCallback = useCallback(() => {
4
handleClick(a);
5
}, [a]);

这些 Hooks 通过缓存依赖不变的值或函数,减少了不必要的重新计算,从而提升了应用性能。

6. 函数组件的生命周期模拟

函数组件没有类组件的生命周期方法,但通过 useEffect,我们可以实现类似的生命周期功能:

  • componentDidMountcomponentDidUpdate:使用 useEffect 模拟,它在组件挂载或更新时执行。
  • componentWillUnmount:通过在 useEffect 中返回清理函数来模拟。
1
useEffect(() => {
2
// 挂载或更新时执行
3
return () => {
4
// 卸载时执行
5
};
6
}, [dependencies]); // 依赖数组控制执行时机

总结

React 函数组件的更新过程从状态变化开始,通过 Fiber 树和调和算法逐步完成对虚拟 DOM 的更新,再反映到实际 DOM 中。Hooks 的引入使函数组件更加简洁,但也需要开发者更加关注性能优化,如合理使用 useMemouseCallbackReact.memo

理解这些底层机制能帮助我们编写更加高效的 React 应用,避免不必要的性能瓶颈。

美团外卖红包 饿了么红包 支付宝红包