Skip to content

1. 对虚拟 DOM 的理解:它是什么,主要做了什么?

虚拟 DOM(Virtual DOM)本质上是用 JavaScript 对象描述 UI 的一层中间表示。它不是真实 DOM,而是“页面此刻应该长什么样”的结构化描述。

例如这段 JSX:

jsx
<div className="title">Hello</div>

在 React 内部会被转成类似这样的对象:

js
{
  type: "div",
  props: {
    className: "title",
    children: "Hello"
  }
}

stateprops 变化时,React 会重新执行组件,生成新的虚拟 DOM,然后和上一次的结果进行比较,最后只把必要的变更提交到真实 DOM。

虚拟 DOM 主要解决了什么问题?

  1. 把 UI 更新从“手写 DOM 操作”变成“声明状态变化”
    开发者只需要描述“数据变了,界面应该长什么样”,不需要自己维护 DOM 的增删改查。

  2. 让框架接管更新过程,给出稳定的性能下限
    框架可以通过批量更新、Diff、节点复用等手段,避免很多低效的 DOM 操作。

  3. 提供跨平台的统一抽象
    因为虚拟 DOM 本质是 JavaScript 对象,所以它可以映射到浏览器 DOM、服务端渲染结果,甚至 React Native 的原生控件。

需要注意的点

  • 虚拟 DOM 不是“一定比原生 DOM 更快”。
  • 如果只是一次非常明确、非常小的 DOM 修改,直接操作原生 DOM 往往更快。
  • 虚拟 DOM 的优势更多体现在:
    • 声明式开发体验
    • 大型应用的可维护性
    • 多次更新下更稳定的整体性能
    • 跨平台能力

一句话总结:虚拟 DOM 的核心价值不是绝对性能,而是用可预测的方式管理 UI 更新。

2. React Diff 算法的原理是什么?

React 的 Diff 通常也叫 Reconciliation(协调)。它不是去求“理论上的最优树编辑距离”,而是采用一套工程化的启发式策略,在可接受的复杂度内尽快找出应该更新的部分。

核心目标

  • 尽可能复用已有节点
  • 尽量减少真实 DOM 操作
  • 在复杂度和更新效果之间做工程化平衡

React Diff 的三个核心结论

1. 只做同层比较,不做跨层级的复杂对比

React 默认认为跨层级移动节点的场景很少,所以它主要比较同一层级的节点。
如果一个节点从这一层移动到了另一层,React 往往会把它当成“旧节点删除 + 新节点创建”,而不是做复杂的全树搜索。

2. 不同类型的节点,直接视为两棵不同的子树

比如从:

jsx
<div>
  <Counter />
</div>

变成:

jsx
<span>
  <Counter />
</span>

根节点类型从 div 变成 span,React 会认为整棵子树都需要重新处理,而不是在原节点上做细粒度复用。

对于组件也是一样:

  • 同类型组件:尽量复用,继续往下比较
  • 不同类型组件:卸载旧组件,挂载新组件

3. 同层级列表节点通过 key 识别身份

列表更新时,React 会优先看 key
如果 key 稳定,就能知道“哪个节点是复用、哪个是新增、哪个是删除、哪个只是移动位置”。

一个简单例子

jsx
function Example({ isVisible }) {
  return isVisible
    ? <div className="visible">visible</div>
    : <div className="hidden">hidden</div>;
}

这次更新里,前后两个节点的类型都还是 div,所以 React 不需要把整个节点删掉重建,而是只更新变化的部分:

  • classNamevisible -> hidden
  • 文本内容:visible -> hidden

React 更新的大致流程

  1. state / props 变化,触发组件重新执行
  2. 生成新的 React Element(也可以理解为新的虚拟 DOM)
  3. 和旧的 Fiber 树进行协调(Reconciliation\Diff)
  4. 标记本次更新需要执行的操作,如 PlacementUpdateDeletion
  5. 在 Commit 阶段把这些变化真正应用到 DOM 上

列表 Diff 再补充一句

React 处理同层级子节点时,会先按顺序比较;一旦发现不匹配,再把剩余旧节点放到映射表里,通过 keytype 查找可复用节点,并结合 lastPlacedIndex 判断节点是否需要移动。

一句话总结:React Diff 的核心不是“算得最全”,而是“以足够快的方式找到足够好的更新结果”。

3. React 中的 key 是干什么用的?为什么不能乱写?

key 是 React 给同一层级兄弟节点分配的“稳定身份标识”。
它的作用不是给开发者看的,而是让 React 在 Diff 过程中准确判断每个节点“是谁”。

key 主要解决两类问题

  1. 减少不必要的 DOM 重建
    列表新增、删除、排序时,React 可以根据 key 复用已有节点,而不是把后面的节点全部重新创建。

  2. 避免组件状态错位
    如果列表项里有输入框、勾选状态、本地 state 等,错误的 key 可能导致“明明改的是 A,结果状态跑到 B 身上”。

正确写法

jsx
{list.map((item) => (
  <li key={item.id}>{item.name}</li>
))}

不推荐的写法

jsx
{list.map((item, index) => (
  <li key={index}>{item.name}</li>
))}

数组下标只在“列表稳定、不会新增删除、不会重排”的场景下勉强可用。
一旦头部插入、排序或过滤,index 就会变化,节点身份也会跟着错位。

更不能这样写

jsx
<li key={Math.random()}>{item.name}</li>

随机 key 会让 React 每次都把节点当成全新的节点,直接失去复用能力,性能和状态稳定性都会变差。

一句话总结:key 解决的是列表节点“身份识别”问题,本质上是为了更准确地复用节点。

4. 虚拟 DOM 和直接操作原生 DOM,谁效率更高?

没有绝对答案,要看场景。

直接操作原生 DOM 可能更快的场景

  • 只改一个确定的节点
  • 变更非常小,且你已经明确知道怎么改
  • 不需要框架额外做 Diff、调度和对象创建

这时,手写 DOM 操作可能比“先生成虚拟 DOM,再 Diff,再 Commit”更直接。

虚拟 DOM 更有价值的场景

  • 页面结构复杂
  • 更新频繁
  • 组件层级多
  • 多人协作开发
  • 需要声明式编程和可维护性

因为虚拟 DOM 可以把更新过程统一交给框架处理,避免大量分散、难维护、容易写错的 DOM 操作。

正确结论

  • 单次极限性能看,虚拟 DOM 不一定更快
  • 大型应用的整体收益看,虚拟 DOM 更有工程价值

可以把它理解为:虚拟 DOM 主要保证性能下限,而不是追求所有场景下的性能上限。

React 官方也从来没有把“虚拟 DOM 一定更快”当成核心卖点,它真正的优势是:

  • 更好的开发体验
  • 更清晰的状态驱动 UI 模型
  • 在多数场景下可接受且稳定的性能表现

5. React 和 Vue 的 Diff 有什么不同?

两者的共同点是:

  • 都使用虚拟 DOM 描述 UI
  • 都通过 key 辅助同层级列表比较
  • 都追求“尽量复用节点,减少真实 DOM 操作”

React 的特点

  • 更偏运行时协调
    React 组件重新执行后,在运行时生成新的 UI 描述,再做协调。

  • 更强调通用的启发式规则
    比如同层比较、类型不同直接替换、列表依赖 key

  • 组件模型更中心化
    React 的更新优化通常和组件拆分、memo、状态提升等设计配合使用。

Vue 3 的特点

  • 更强调编译时优化
    Vue 模板在编译阶段就会标记哪些节点是静态的、哪些是动态的。

  • 运行时需要 Diff 的内容更少
    例如静态节点提升、Patch Flags、Block Tree 等机制,都会减少无意义比较。

  • 列表更新会做更细的移动优化
    Vue 3 在 keyed children 的 Diff 中会利用最长递增子序列(LIS)来尽量减少 DOM 移动。

这道题可以怎么回答

一句话版:

  • React 更偏“运行时的通用协调”
  • Vue 3 更偏“编译时 + 运行时联合优化”

所以在很多模板场景下,Vue 3 能提前跳过不少不必要的 Diff;而 React 的优势在于模型统一、灵活性高、组件化表达自然。

6. React 是怎么完成渲染和更新的?

React 的渲染可以拆成两个关键词:Render 阶段Commit 阶段

1. 首次渲染

以这段代码为例:

jsx
function App() {
  return (
    <div>
      <h1>Hello, React</h1>
      <p>欢迎学习 React</p>
    </div>
  );
}

首次渲染时大致会经历:

  1. 执行组件函数,得到 JSX 对应的 React Element
  2. React 基于这些元素创建 Fiber 节点,组织出 Fiber 树
  3. 在 Render 阶段计算整棵树应该如何渲染
  4. 在 Commit 阶段创建真实 DOM,并插入页面

2. 更新渲染

stateprops 变化时:

  1. React 调度一次更新
  2. 重新执行受影响的组件,生成新的 React Element
  3. 基于新旧 Fiber 树进行 Reconciliation
  4. 计算出最小必要变更
  5. 在 Commit 阶段把变更同步到真实 DOM,并处理副作用

3. Fiber 起到了什么作用?

Fiber 可以把一次渲染拆成更细的工作单元,让 React 能够:

  • 给不同更新分配优先级
  • 在 Render 阶段中断、恢复、重试任务
  • 避免长任务长期阻塞主线程

需要注意的是:

  • Render 阶段 可以被打断
  • Commit 阶段 一旦开始,就会同步执行完

4. 为什么 Fiber 重要?

React 16 以前的更新更接近“整棵树同步递归执行”,一旦组件树很大,就容易阻塞主线程。
Fiber 的意义在于把“不可中断的大任务”改造成“可调度的小任务”,这也是后续并发渲染能力的基础。

一句话总结:React 不是每次都重建整个 DOM,而是通过 Fiber + Reconciliation 找出最小变更,再在 Commit 阶段更新真实 DOM。

7. 高频面试总结

  • 虚拟 DOM:用 JavaScript 对象描述 UI,是 React 的中间表示层。
  • Diff:基于启发式规则做同层比较,尽量复用节点。
  • key:解决同层级列表节点的身份识别问题。
  • Fiber:让 Render 阶段可中断、可调度、有优先级。
  • 性能结论:虚拟 DOM 不一定更快,但能提供更稳定的性能下限和更好的工程体验。
  • React vs Vue:React 偏运行时协调,Vue 3 偏编译时优化 + 运行时优化。

评论区

欢迎留言、补充或勘误。

xiaoba.blog