1. React 渲染流程概述
React 的渲染过程可以分为两个主要阶段:Render 阶段和 Commit 阶段。这个设计是 React 高效渲染的核心机制,也是面试中经常被问到的重点内容。
Render 阶段负责计算变化,Commit 阶段负责将这些变化应用到 DOM 上。这种分离的设计使得 React 能够实现高效的更新机制,避免不必要的 DOM 操作。
提示:React 16 引入的 Fiber 架构正是基于这两个阶段的划分,使得 React 能够实现增量渲染和任务优先级调度。
2. Render 阶段详解
2.1 Render 阶段的核心任务
Render 阶段的主要工作是计算组件树中哪些部分需要更新。这个阶段是纯计算的,不会产生任何实际的 DOM 操作。具体来说,Render 阶段会:
- 调用组件的 render 方法生成新的虚拟 DOM
- 对比新旧虚拟 DOM,找出需要更新的部分
- 为需要更新的组件打上 effectTag 标记
2.2 Fiber 架构在 Render 阶段的作用
Fiber 是 React 16 引入的新协调引擎,它为每个组件实例创建了一个 Fiber 节点。在 Render 阶段:
- React 会遍历 Fiber 树,为每个 Fiber 节点执行工作
- 采用深度优先遍历算法,可以中断和恢复遍历过程
- 构建 effect list(副作用链表),记录所有需要更新的节点
javascript复制function performUnitOfWork(fiber) {
// 1. 开始工作(beginWork)
// 2. 完成工作(completeWork)
// 3. 返回下一个工作单元
}
2.3 协调算法(Reconciliation)
协调算法是 Render 阶段的核心,它决定了如何高效地更新 UI。React 使用以下策略:
- 同级比较:只比较同一层级的节点
- key 值优化:使用 key 识别节点身份,提高复用率
- 类型比较:如果元素类型不同,直接卸载重建
3. Commit 阶段详解
3.1 Commit 阶段的工作流程
Commit 阶段是 React 实际更新 DOM 的阶段,它分为三个子阶段:
- Before mutation 阶段:执行 DOM 操作前的准备工作
- Mutation 阶段:执行实际的 DOM 操作
- Layout 阶段:执行 DOM 操作后的工作
javascript复制function commitRoot(root) {
// Before mutation 阶段
commitBeforeMutationEffects();
// Mutation 阶段
commitMutationEffects();
// Layout 阶段
commitLayoutEffects();
}
3.2 Before mutation 阶段
这个阶段主要完成以下工作:
- 获取 effect list(在 Render 阶段构建的副作用链表)
- 重置一些内部状态(如 workInProgress 树)
- 处理 passive effects(useEffect 相关的调度)
3.3 Mutation 阶段
Mutation 阶段是实际修改 DOM 的阶段,主要工作包括:
- 插入、更新或删除 DOM 节点
- 更新 ref 引用
- 执行 componentWillUnmount 生命周期
注意:在 React 17+ 中,componentWillUnmount 实际上是在这个阶段执行的,而不是在卸载阶段。
3.4 Layout 阶段
Layout 阶段在 DOM 更新后立即执行,主要工作包括:
- 调用 componentDidMount 和 componentDidUpdate
- 执行 useLayoutEffect 的回调
- 更新 ref 引用(第二次更新)
4. 两阶段设计的优势与实现细节
4.1 为什么需要分离 Render 和 Commit
这种分离设计带来了几个关键优势:
- 可中断的渲染:Render 阶段可以被中断,不会阻塞主线程
- 批量更新:多个更新可以在一次 Commit 中完成
- 一致性保证:确保 DOM 和 React 状态始终一致
4.2 Effect List 的构建与使用
Effect list 是连接 Render 和 Commit 阶段的关键数据结构:
- 在 Render 阶段,React 会为有变化的 Fiber 节点打上 effectTag
- 这些节点会被连接成一个单向链表(effect list)
- Commit 阶段遍历这个链表来执行 DOM 操作
4.3 生命周期与 Hook 的执行时机
理解不同生命周期和 Hook 的执行时机非常重要:
-
Render 阶段:
- 执行组件的 render 方法
- 执行 useReducer 和 useState 的更新函数
-
Commit 阶段:
- Before mutation:无直接暴露的 API
- Mutation:componentWillUnmount(class)
- Layout:componentDidMount/Update(class),useLayoutEffect
-
异步执行:
- useEffect 的回调会在 Commit 完成后异步执行
5. 性能优化与常见问题
5.1 如何优化 Render 阶段性能
- 使用 React.memo 或 PureComponent 避免不必要的渲染
- 合理使用 useMemo 和 useCallback
- 避免在 render 方法中进行昂贵的计算
5.2 Commit 阶段的性能考量
- 减少 DOM 操作的总量
- 批量更新状态(使用 unstable_batchedUpdates)
- 对于大型列表,考虑虚拟滚动方案
5.3 常见问题排查
-
无限循环更新:
- 检查是否在 render 中直接调用了 setState
- 确保 useEffect 和 useLayoutEffect 的依赖项正确
-
布局抖动(Layout thrashing):
- 避免在 useLayoutEffect 中触发同步重排
- 将读取和写入 DOM 的操作分开
-
Effect 执行顺序问题:
- 理解 useEffect 和 useLayoutEffect 的执行时机差异
- 注意 effect 的清理函数执行顺序
6. 实际案例分析
6.1 一个典型的更新流程
让我们通过一个具体的例子来看两阶段如何工作:
-
用户点击按钮触发状态更新
-
Render 阶段:
- React 从根节点开始协调
- 计算哪些组件需要更新
- 标记变化的节点并构建 effect list
-
Commit 阶段:
- Before mutation:准备更新
- Mutation:实际更新 DOM
- Layout:执行生命周期和 layout effects
6.2 useLayoutEffect vs useEffect
这两个 Hook 的主要区别在于执行时机:
javascript复制function Example() {
const [width, setWidth] = useState(0);
useLayoutEffect(() => {
// 在 DOM 更新后、浏览器绘制前执行
const newWidth = measureWidth();
setWidth(newWidth);
}, []);
useEffect(() => {
// 在浏览器绘制后执行
trackAnalytics();
}, []);
return <div style={{width}} />;
}
6.3 并发模式下的两阶段渲染
在并发模式下,这两个阶段的工作方式有所变化:
- Render 阶段可以被中断和恢复
- React 可以准备多个版本的 UI,但只 Commit 其中一个
- 使用 transition API 来区分紧急和非紧急更新
7. 深入源码解析
7.1 Render 阶段的入口
React 的 Render 阶段从 performSyncWorkOnRoot 或 performConcurrentWorkOnRoot 开始:
javascript复制function performSyncWorkOnRoot(root) {
// 准备新的 workInProgress 树
prepareFreshStack(root);
// 开始渲染工作循环
workLoopSync();
// 渲染完成后进入 Commit 阶段
commitRoot(root);
}
7.2 workLoop 的实现
workLoop 是 Render 阶段的核心循环:
javascript复制function workLoopSync() {
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
function workLoopConcurrent() {
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
7.3 Commit 阶段的入口
Commit 阶段从 commitRoot 函数开始:
javascript复制function commitRoot(root) {
// 准备 effect list
const finishedWork = root.finishedWork;
const effectList = finishedWork.firstEffect;
// Before mutation 阶段
commitBeforeMutationEffects(effectList);
// Mutation 阶段
commitMutationEffects(effectList);
// Layout 阶段
commitLayoutEffects(effectList);
}
8. 最佳实践与经验分享
8.1 如何合理组织组件结构
- 将频繁更新的组件与稳定组件分离
- 使用 children prop 来避免不必要的重新渲染
- 考虑将状态尽量下放到叶子组件
8.2 状态管理的选择
- 对于局部状态,优先使用 useState/useReducer
- 对于全局状态,考虑 Context API 或状态管理库
- 避免在深层组件树中使用多个 Context
8.3 调试技巧
- 使用 React DevTools 分析组件更新原因
- 使用 Profiler API 测量渲染性能
- 通过添加日志点来跟踪渲染流程:
javascript复制useEffect(() => {
console.log('Commit 阶段完成后执行');
return () => console.log('清理上一次的 effect');
}, [deps]);
useLayoutEffect(() => {
console.log('Layout 阶段同步执行');
}, [deps]);
在实际项目中,理解 React 的两阶段渲染流程对于性能优化和问题排查至关重要。特别是在处理复杂交互和动画时,合理利用 useLayoutEffect 和 useEffect 的执行时机差异,可以避免很多常见的 UI 问题。
