跳到主要内容

组件渲染过程

面试速答(30 秒版 TL;DR)

  • 以下内容以 React 18/19 的 Fiber 架构为背景。
  • React 组件渲染不要只答成“执行组件函数”,完整链路是:触发更新 -> render 阶段生成新 Fiber 树 -> reconciliation 比较差异 -> commit 阶段提交变更 -> effect 在提交后执行
  • 首次渲染和更新渲染的差别在于:
    • 首次渲染主要是创建 Fiber 和挂载真实 DOM。
    • 更新渲染主要是复用旧 Fiber、比较新旧子树、按需更新 DOM。
  • render 阶段本质上是“算结果”,可以被中断;commit 阶段本质上是“落地结果”,不能被打断。
  • 一句话总结:React 组件渲染过程,本质是 React 先算出下一版 UI,再一次性把必要变更提交到页面。

一、先建立正确心智模型

很多人一提“组件渲染”,脑子里只有一句:

React 会重新执行函数组件。

这句话不算错,但太窄了。

更完整的理解应该是:

  1. React 先收到一次更新请求。
  2. React 在内部创建或复用 Fiber 工作节点。
  3. React 重新计算这棵子树下一版应该长什么样。
  4. React 比较新旧结果,找出最小必要变更。
  5. React 在 commit 阶段一次性修改宿主环境,比如浏览器 DOM。

所以“渲染”不是单指某一个动作,而是一整条更新流水线。

二、哪些操作会触发组件渲染

常见触发源有 4 类:

  • setState / useState 更新
  • 父组件重新渲染,导致子组件收到新的 props
  • context 值变化
  • forceUpdate、Suspense 重试、错误恢复等内部机制

注意一个常见误区:

  • 触发更新 不等于 一定提交 DOM 变化

因为 React 会先重新计算。如果前后结果相同,或者某一层可以 bailout,最终可能不会产生真实 DOM 改动。

三、首次挂载时,组件是怎么渲染出来的

首次渲染可以理解成“从无到有建整棵树”。

1. 创建根更新任务

当你调用:

root.render(<App />)

React 会从根节点开始创建更新任务,并准备构建一棵新的 Fiber 树。

2. 执行组件,得到 React Element

如果是函数组件,React 会调用组件函数:

function App() {
return <div>Hello</div>
}

组件函数执行后,并不会直接返回真实 DOM,而是返回一组 React Element 描述。

你可以粗略理解为:

  • JSX 先编译成 React Element
  • React 再基于 Element 创建 Fiber 节点
  • Fiber 再指导后续 DOM 挂载

3. 向下递归构建 Fiber 子树

React 会继续处理组件返回的子节点:

  • 遇到函数组件,就继续执行它
  • 遇到原生标签,比如 div,就创建宿主 Fiber
  • 遇到文本节点,就创建文本 Fiber

这一步发生在 render 阶段,本质是把“组件树描述”展开成“可执行的 Fiber 工作树”。

4. 为宿主节点准备创建信息

在首次挂载里,因为旧树不存在,所以 React 主要做的是:

  • 创建需要的 Fiber 节点
  • 给宿主节点打上插入标记
  • 准备要创建哪些 DOM 节点

5. commit 阶段真正挂载 DOM

当整棵 workInProgress 树准备好后,React 进入 commit

这一阶段会:

  • 创建真实 DOM
  • 插入到正确位置
  • 绑定 ref
  • 执行布局相关副作用

到这里,用户才真正能在页面上看到内容。

四、更新时,组件是怎么重新渲染的

更新渲染比首次挂载更有面试价值,因为它涉及复用、Diff 和调度。

1. 更新先进入队列,不会立刻同步改 DOM

比如:

const [count, setCount] = useState(0)

function handleClick() {
setCount(count + 1)
}

调用 setCount 后,React 先做的是:

  • 记录这次更新
  • 标记对应 Fiber 有新任务
  • 按优先级安排后续工作

它不会在你调用 setCount 的那一行立刻把 DOM 改掉。

2. 从当前树派生 workInProgress 树

React 内部一般会同时维护两棵树:

  • current:当前已提交到页面的 Fiber 树
  • workInProgress:这次更新正在计算的新树

更新时,React 会基于 current 派生出 workInProgress,然后在新树上做计算。

3. 重新执行受影响的组件

当轮到某个函数组件时,React 会再次执行这个组件函数,拿到新的 React Element 树。

这里要强调两点:

  • 重新执行组件函数,不代表一定重建整棵 DOM
  • 子组件是否继续深入处理,要看 propsstatecontextmemo 等条件

也就是说,React 的“重渲染”首先是 JavaScript 层重新计算,不是 DOM 层全部重做。

4. reconciliation 比较新旧子树

拿到新的 Element 结果后,React 会和旧 Fiber 对应的结果做比较。

比较时通常重点看:

  • 节点类型是否相同
  • key 是否稳定
  • 属性是否变化
  • 子节点顺序是否变化

常见结果有 3 类:

  • 复用已有节点,只更新属性
  • 新建节点并插入
  • 删除旧节点

这一步就是常说的协调(reconciliation)。

5. 收集副作用标记

render 阶段,React 不急着改 DOM,而是先把这次需要做的事记录下来,比如:

  • 插入
  • 更新
  • 删除
  • ref 变化

这些信息通常会体现在 Fiber 的副作用标记里,等到 commit 阶段统一处理。

五、render 阶段到底在做什么

面试里最容易拉开差距的,是你能不能把 render 阶段讲清楚。

render 阶段主要做 3 件事:

  1. 执行组件,得到新的 UI 描述
  2. 构建新的 Fiber 子树
  3. 比较新旧节点并收集副作用

它的特点是:

  • 可以被打断
  • 可以被恢复
  • 结果在提交前还不对用户可见

所以现代 React 说的“并发渲染”,核心不是多线程,而是:

  • render 阶段的工作可以被拆分调度

例如输入框高优先级更新到来时,低优先级列表刷新可以稍后继续。

六、commit 阶段到底在做什么

commit 阶段的关键词只有一个:提交

它主要负责:

  • 把 DOM 插入、更新、删除真正执行掉
  • 处理 ref
  • 执行 useLayoutEffect 和相关同步副作用
  • 在浏览器绘制后安排 useEffect

可以把提交顺序粗略记成:

  1. DOM 变更
  2. ref
  3. useLayoutEffect
  4. 浏览器绘制
  5. useEffect

这也是为什么:

  • useLayoutEffect 更接近“提交后立刻同步执行”
  • useEffect 更接近“绘制后再执行的副作用”

七、为什么组件函数执行了,但页面不一定变化

这是高频追问。

因为 React 的更新分成“计算”和“提交”两层。

组件函数执行,只说明 React 在重新计算下一版 UI;但如果比较后发现:

  • 新旧结果一致
  • 某个 Fiber 可以 bailout
  • 某次更新被更高优先级更新覆盖

那最终 DOM 可能几乎不变,甚至完全不变。

所以要把这几个概念分开:

  • 组件重新执行
  • Fiber 重新计算
  • 真实 DOM 更新

它们不是同义词。

八、首次渲染和更新渲染怎么答区别

面试里可以直接这样答:

维度首次渲染更新渲染
旧树是否存在不存在存在
主要目标挂载整棵树复用并最小化变更
render 阶段重点创建 Fiber、准备宿主节点比较新旧子树、决定复用与删除
commit 阶段重点创建并插入 DOM按标记更新、移动、删除 DOM
性能关键点初始构建成本Diff、bailout、优先级调度

九、面试标准答法

题 1:React 组件渲染过程是怎样的?

React 组件渲染可以分成两大阶段。先是 render 阶段,React 会根据状态、props、context 的变化,从当前 Fiber 树派生出 workInProgress 树,重新执行相关组件,生成新的 React Element,并通过 reconciliation 比较新旧子树,收集这次更新需要执行的副作用。然后进入 commit 阶段,把真正的 DOM 变更、ref 和 effect 提交出去。render 阶段可以被中断,commit 阶段必须一次完成。首次渲染偏向创建和挂载,更新渲染偏向复用和最小化变更。

题 2:React 重新渲染一定会改 DOM 吗?

不会。重新渲染首先是组件和 Fiber 层面的重新计算,React 会先比较新旧结果,只有真正有差异时才会在 commit 阶段修改 DOM。

题 3:为什么 render 阶段可以中断,commit 不能中断?

因为 render 只是计算下一版结果,中断后还能继续算;commit 是把结果真正提交到页面,如果中断,界面会落在半更新状态,产生不一致。

十、常见追问

1. 父组件渲染了,子组件一定渲染吗?

默认要看一遍子树,但不代表一定产生 DOM 更新。若子组件用了 React.memo,且 props 没变,就可能在该层 bailout。

2. key 在渲染过程中有什么用?

key 主要帮助 React 在同层子节点比较时识别“谁是谁”,从而决定复用、移动还是删除,尤其影响列表更新质量。

3. useEffect 算渲染过程的一部分吗?

算整个更新链路的一部分,但它不属于 render 阶段,而是提交后的副作用阶段。

4. React 组件函数执行时能直接操作 DOM 吗?

不应该。组件函数运行在 render 阶段,这一阶段要求尽量纯,因为它可能被重复执行、打断或丢弃。

十一、易错点 / 坑

  • 把“组件渲染”简化成“执行函数组件”。
  • 把“重渲染”误解成“整个 DOM 全量重建”。
  • 分不清 render 阶段和 commit 阶段。
  • 说不出首次挂载和更新渲染的区别。
  • useEffect 当成 render 内同步执行逻辑。

速记要点(可背诵)

  • 触发更新,不等于立刻改 DOM。
  • render 负责算下一版 UI,可中断。
  • reconciliation 负责比较新旧子树。
  • commit 负责真正提交 DOM / ref / effect。
  • 首次渲染偏挂载,更新渲染偏复用。