跳到主要内容

组件渲染过程

面试速答(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
  • 子组件是否继续深入处理,要看 props、state、context、memo 等条件

也就是说,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。
  • 首次渲染偏挂载,更新渲染偏复用。