React 函数组件是如何触发更新的
- 1520字
- 8分钟
- 2024-09-21
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)。当调用 setState
或 useState
的 setter 函数时,React 会将这个更新存入 Fiber 节点的更新队列中,等待调度器来执行。
3. React 调度更新的过程
当函数组件的状态或 props 发生变化时,React 会进入更新调度过程。核心流程如下:
3.1 setState 及 useState 的更新机制
当 useState
中的 setter 方法(如 setState
)被调用时,实际上会创建一个 更新对象,该对象包含了新的状态值以及需要更新的组件引用。这个更新对象会被加入当前 Fiber 节点的更新队列中,等待 React 调度。
1// 简化后的 useState 实现2function 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。每个 useState
、useEffect
调用都会在这条链表上创建或复用一个 hook 节点,从而存储状态值或副作用。
1function 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) 来决定哪些部分需要更新。调和的核心步骤包括:
- 创建新的虚拟 DOM:每次组件更新时,React 会根据新的状态和 props 生成一棵新的虚拟 DOM 树。
- Diffing 阶段:React 比较新旧两棵虚拟 DOM 树,通过 Diff 算法找出需要修改的部分。
- 更新实际 DOM:React 将差异最小化后,批量更新实际的 DOM。
Fiber 树使 React 可以在工作单元(更新步骤)之间暂停和恢复,优化了长任务的执行过程,提高了应用的响应性。
4.2 更新优先级与调度
React 使用 优先级队列 来控制更新的调度顺序。每次状态或 props 变化时,React 会将更新任务赋予一个优先级,根据任务的紧急程度决定何时执行。
- 高优先级更新(如用户输入事件)会立即执行。
- 低优先级更新(如动画或后台数据加载)则会延后执行,保证 UI 的流畅性。
5. 函数组件性能优化:useMemo 与 useCallback
由于每次组件更新都会重新执行整个函数,因此使用 useMemo
和 useCallback
来缓存一些昂贵的计算或函数引用非常关键。
useMemo
:用于缓存计算结果,避免在每次渲染时重复计算。useCallback
:用于缓存函数,避免在子组件中每次都创建新的函数引用。
1const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);2
3const memoizedCallback = useCallback(() => {4 handleClick(a);5}, [a]);
这些 Hooks 通过缓存依赖不变的值或函数,减少了不必要的重新计算,从而提升了应用性能。
6. 函数组件的生命周期模拟
函数组件没有类组件的生命周期方法,但通过 useEffect
,我们可以实现类似的生命周期功能:
componentDidMount
和componentDidUpdate
:使用useEffect
模拟,它在组件挂载或更新时执行。componentWillUnmount
:通过在useEffect
中返回清理函数来模拟。
1useEffect(() => {2 // 挂载或更新时执行3 return () => {4 // 卸载时执行5 };6}, [dependencies]); // 依赖数组控制执行时机
总结
React 函数组件的更新过程从状态变化开始,通过 Fiber 树和调和算法逐步完成对虚拟 DOM 的更新,再反映到实际 DOM 中。Hooks 的引入使函数组件更加简洁,但也需要开发者更加关注性能优化,如合理使用 useMemo
、useCallback
和 React.memo
。
理解这些底层机制能帮助我们编写更加高效的 React 应用,避免不必要的性能瓶颈。


