> ## Documentation Index
> Fetch the complete documentation index at: https://opc-43d279b8.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Vue Diff

> 讲清 Vue 2 双端比较与 Vue 3 前后缀同步加最长递增子序列的实现差异，覆盖 key、PatchFlag 与 Block Tree。

> diff 这块我读过几遍源码才顺：Vue 2 的双端比较和 Vue 3 的最长递增子序列不是一回事，key 用 index 会在列表重排时出问题。

***

## Q1: 为什么要有虚拟 DOM？

**1. jQuery 时代: 手动操作 DOM**

在 jQuery 时代，开发者需要手动调用 `document.getElementById`、`appendChild`、`removeChild` 等 API 来操作 DOM。代码充斥着命令式的 DOM 操作逻辑，和业务逻辑耦合在一起，难以维护。

**2. 致命痛点: 频繁 DOM 操作性能爆炸**

每次 DOM 操作都可能触发浏览器的重排（reflow）和重绘（repaint）。一个复杂的列表更新如果逐条操作 DOM，会产生上百次重排，页面直接卡顿。

**3. 虚拟 DOM 的引入**

虚拟 DOM 是一个轻量的 JS 对象，用来描述真实 DOM 的结构。框架先在内存中计算出新旧两棵虚拟 DOM 树的差异，再一次性把最小的变更批量应用到真实 DOM，大幅减少 DOM 操作次数。

**4. 声明式编程的基石**

有了虚拟 DOM，开发者只需声明"数据是什么样的"（`{{ msg }}`），框架自动帮你计算"怎么更新 DOM"。这种声明式编程模型彻底解放了开发者的心智负担。

**5. 跨平台能力**

虚拟 DOM 本质是 JS 对象，不依赖浏览器 API。Vue 可以把它渲染到 Web、Native（Weex/UniApp）、Canvas 甚至服务端（SSR）。这是虚拟 DOM 的附加价值，但不是它的核心设计动机。

> [!TIP]
>
> 虚拟 DOM 就像建筑师在动手盖房子之前，先用泡沫板做一个建筑模型。你可以在模型上随意调整窗户位置、改墙体颜色（修改虚拟 DOM），反复试错零成本。等方案定好了（diff 计算出差异），施工队才去改真实房子（更新真实 DOM），而且只改需要动的地方。没有虚拟 DOM 的话，你每次想改个窗户都得直接拆墙，代价大、风险高。

**虚拟 DOM 解决的核心问题：**

### 虚拟 DOM 的本质：声明式编程的中间层

虚拟 DOM 不是一个性能优化方案——手写最优的 DOM 操作一定比虚拟 DOM 快。虚拟 DOM 的真正价值是**在保证足够好的性能的同时，提供极大的开发体验提升**。尤雨溪在 Vue 官方文档中明确指出：虚拟 DOM 的意义在于"提供一个声明式的、可组合的抽象层，让开发者专注于状态管理而非 DOM 操作"。

Vue 3 在保留虚拟 DOM 的同时，通过编译时优化（PatchFlag、Block Tree、静态提升）大幅减少了运行时开销，让模板编译后的代码在大多数场景下接近甚至超过手写 render 函数的性能。

```text theme={null}
问题：手动操作 DOM = 命令式 + 频繁重排

开发者需要：
  1. 找到 DOM 节点
  2. 判断哪些数据变了
  3. 手动计算最小 DOM 操作
  4. 逐个执行 DOM 操作
  → 代码难写、难维护、性能难保证

解决：虚拟 DOM + Diff

开发者只需：
  声明数据状态（模板/JSX）
  → 框架自动生成虚拟 DOM
  → Diff 算法比较新旧差异
  → 最小批量更新真实 DOM
```

> [!NOTE]
>
> 虚拟 DOM 是声明式编程的桥梁——开发者声明状态，框架自动高效更新 DOM。

> [card] 没有虚拟 DOM
>
> 你需要手动计算每次数据变化对应的 DOM 操作。想象你是一个仓库管理员，每次货架上少了 3 件商品，你得自己拿纸笔对比账本和货架，逐个核对每个商品的数量差异，再一个个上架补货。

> [card] 引入虚拟 DOM
>
> 框架帮你自动对比"账本"和"货架"的差异，一次性告诉你"只需要补 A 商品 2 件、B 商品 1 件"。你只管更新数据（账本），diff 算法帮你算出最小变更清单，批量执行。

> [!TIP]
>
> 回答时先说"为什么"（声明式编程 + 批量更新），再说"怎么做"（虚拟 DOM + diff）。想听的是你对声明式编程为什么需要虚拟 DOM 这个中间层的理解，而不是背"虚拟 DOM 比真实 DOM 快"——这句话本身是错的。

***

## Q2: Vue 2 vs Vue 3 的 diff 算法有什么区别？

### 双端比较 vs LIS 优化

Vue 2 使用**双端比较**算法（新旧头尾四个指针），Vue 3 改为**前后缀同步 + 中间乱序部分的最长递增子序列（LIS）**：

```text theme={null}
Vue 2 双端比较：
┌───────────────────────────────────────┐
│  旧: A  B  C  D  E                   │  ← 4 个指针：旧头、旧尾、新头、新尾
│  新: A  D  B  E  C                   │
│                                       │
│  1. 旧头A == 新头A ✓ 双指针右移      │
│  2. 旧尾E != 新尾C ✗                  │
│  3. 旧头B != 新尾C ✗                  │
│  4. 旧尾E != 新头D ✗                  │
│  5. 用 key 在旧列表查找 D → 移动      │
│  6. 用 key 在旧列表查找 C → 移动      │
└───────────────────────────────────────┘

Vue 3 LIS 优化：
┌───────────────────────────────────────┐
│  旧: A  B  C  D  E     索引: 0,1,2,3,4│
│  新: A  D  B  E  C                    │
│                                       │
│  1. 左→右逐一比较：A==A ✓, B!=D ✗ 跳出│
│  2. 右→左逐一比较：E!=C ✗ 跳出         │
│  3. 中间乱序部分：                      │
│  建 keyToNewIndexMap: D→1,B→2,E→3,C→4 │
│  按新索引填 newIndexToOldIndexMap:     │
│     [3, 1, 4, 2]                      │
│     最长递增子序列: [1, 4] (B和E不动)  │
│     只移动 D 和 C                      │
└───────────────────────────────────────┘
```

```text theme={null}
Vue 3 diff 算法的三步走图解：

  旧列表: [A] [B] [C] [D] [E] [F] [G]
  新列表: [A] [B] [D] [F] [C] [E] [G]
            ↑                        ↑
          i=0                      e1/e2

  第一步：从左到右，处理头部相同部分（O(1) 每次比较）
  ┌───┬───┬───┬───┬───┬───┬───┐
  │ A │ B │ C │ D │ E │ F │ G │  旧
  ├───┼───┼───┼───┼───┼───┼───┤
  │ A │ B │ D │ F │ C │ E │ G │  新
  └───┴───┴───┴───┴───┴───┴───┘
  ✓   ✓   ✗
  A==A  B==B  C!=D → 跳出，i=2

  第二步：从右到左，处理尾部相同部分
  ┌───┬───┬───┬───┬───┬───┬───┐
  │ A │ B │ C │ D │ E │ F │ G │  旧
  ├───┼───┼───┼───┼───┼───┼───┤
  │ A │ B │ D │ F │ C │ E │ G │  新
  └───┴───┴───┴───┴───┴───┴───┘
                  ✗       ✓   ✓
              E!=C  F==F  G==G → e1=4, e2=4

  第三步：中间乱序部分 [C,D,E] vs [D,F,C,E]
  ┌─────────────────────────────────────────────┐
  │ 1. 建立 key→新索引 的 Map: D→2, F→3, C→4, E→5│
  │ 2. 遍历旧节点，查找可复用:                     │
  │    C → newIndex=4, 记录 [4]                   │
  │    D → newIndex=2, 记录 [4,2]                 │
  │    E → newIndex=5, 记录 [4,2,5]               │
  │ 3. 最长递增子序列(LIS): [4,5] → C和E不动      │
  │ 4. 只移动 D 和 F                              │
  └─────────────────────────────────────────────┘
```

```javascript theme={null}
// Vue 3 的 diff 核心逻辑（伪代码，每行注释）
function patchKeyedChildren(c1, c2, container) {
  let i = 0;
  let e1 = c1.length - 1;
  let e2 = c2.length - 1;

  // ===== 第一步：从左到右，处理头部相同的部分 =====
  // 用 i 指针从头部开始，逐一比较新旧节点的 key 和 type
  while (i <= e1 && i <= e2) {
    if (isSameVNode(c1[i], c2[i])) {
      patch(c1[i], c2[i]); // 相同则 patch 更新
      i++;
    } else break; // 不同则跳出，进入下一步
  }

  // ===== 第二步：从右到左，处理尾部相同的部分 =====
  // 用 e1（旧尾）和 e2（新尾）从尾部开始，逐一比较
  while (i <= e1 && i <= e2) {
    if (isSameVNode(c1[e1], c2[e2])) {
      patch(c1[e1], c2[e2]);
      e1--;
      e2--;
    } else break; // 不同则跳出，进入第三步
  }

  // ===== 第三步：中间乱序部分，用 LIS 优化 =====
  const s1 = i; // 旧列表中间段的起点
  const s2 = i; // 新列表中间段的起点

  // 建立 key → 新索引 的 Map，O(1) 查找
  const keyToNewIndexMap = new Map();
  for (let j = s2; j <= e2; j++) {
    keyToNewIndexMap.set(c2[j].key, j);
  }

  // 创建映射数组：newIndexToOldIndexMap[新索引 - s2] = 旧索引 + 1（0 表示新增）
  const toBePatched = e2 - s2 + 1;
  const newIndexToOldIndexMap = new Array(toBePatched).fill(0);

  // 遍历旧节点，在 Map 中查找可复用的节点
  for (let j = s1; j <= e1; j++) {
    const newIndex = keyToNewIndexMap.get(c1[j].key); // O(1) 查找
    if (newIndex !== undefined) {
      newIndexToOldIndexMap[newIndex - s2] = j + 1; // 记录旧索引（+1 避免 0）
    } else {
      unmount(c1[j]); // 旧节点在新列表中不存在，删除
    }
  }

  // 计算最长递增子序列（LIS）—— 这些节点的相对位置没变，不需要移动
  const increasingNewIndexSequence = getSequence(newIndexToOldIndexMap);

  // 从后往前遍历新列表，只移动不在 LIS 中的节点
  let seqIndex = increasingNewIndexSequence.length - 1;
  for (let j = toBePatched - 1; j >= 0; j--) {
    const newIndex = s2 + j;
    if (newIndexToOldIndexMap[j] === 0) {
      patch(null, c2[newIndex]); // 新节点，挂载
    } else if (seqIndex < 0 || j !== increasingNewIndexSequence[seqIndex]) {
      move(c2[newIndex], container); // 不在 LIS 中，移动 DOM
    } else {
      seqIndex--; // 在 LIS 中，保持原位
    }
  }
}
```

### diff 的三条铁律

Vue 的 diff 不是逐节点暴力比较，而是有三条设计约束：

* **同层比较**：只比较同一层级的节点，不跨层级移动。这把时间复杂度从 O(n^3) 降到 O(n)。
* **类型判断**：节点类型（type）不同，直接销毁旧节点、重建新节点，不比较子树。
* **key 标识**：同类型节点用 key 区分身份。没有 key 时 Vue 退化为按索引逐一比较，可能导致状态错乱。

### 为什么前后缀同步能覆盖大多数场景

Vue 3 的前后缀同步（从左到右 + 从右到左）用 O(min(e1, e2)) 的时间覆盖了 80% 的真实场景——因为大多数列表更新都只是尾部增删或中间内容修改，节点位置不变。只有当这两步都无法处理时，才进入第三步的 LIS 计算。React 的单端遍历即使只是尾部加一个元素，也要遍历整个列表建立 key 映射。

### LIS 算法的时间复杂度分析

Vue 3 的 LIS 算法采用动态规划 + 贪心 + 二分查找，时间复杂度 O(n log n)。具体实现中，用一个 `result` 数组维护当前的最长递增子序列，每来一个新元素，用二分查找确定它在 `result` 中的位置——如果比所有元素都大就追加，否则替换第一个比它大的元素。最终 `result` 的长度就是 LIS 的长度。

> [!TIP]
>
> Vue 2 的双端比较就像你搬家时，从书架两头开始配对旧书和新书架上的位置——头对头、尾对尾、头对尾、尾对头，四次配对都不对才去翻箱子找。Vue 3 的 LIS 策略更聪明：你先把所有书编号，找出"本来就不用挪位置的那几本"（最长递增子序列），只搬运剩下的。就像你发现有 10 本书本来就在正确的位置上，只需要动 3 本，搬家效率立刻翻倍。

***

> [!NOTE]
>
> PatchFlag、Block Tree、静态提升等编译器优化的详细原理，详见 [Vue 编译器](https://opc-43d279b8.mintlify.site/vue/vue-compiler.md)。

***

## Q3: 为什么不能用 index 做 key？

### 经典场景：列表中间删除一个元素

```javascript theme={null}
// 初始列表：[{name: '张三'}, {name: '李四'}, {name: '王五'}]

// 用 index 做 key：
// key: [0, 1, 2] → 对应 张三, 李四, 王五
// 删除"张三"后：[{name: '李四'}, {name: '王五'}]
// key: [0, 1]    → 对应 李四, 王五

// Vue 认为：key=0 的节点还在，只是 props 从"张三"变成了"李四"
// 结果：强制更新 key=0 和 key=1 的 props，而不是复用节点
// 如果节点内有输入框，用户输入的内容会错乱！
```

### 用唯一 ID 做 key

```javascript theme={null}
// 删除"张三"后：key=[2, 3] 对应 李四, 王五
// Vue 认为：key=1（张三）被删除了，key=2（李四）和 key=3（王五）保留
// 结果：只删除一个 DOM 节点，李四和王五被完美复用，内部状态不变
```

> [!TIP]
>
> 用 index 做 key 就像老师按座位号点名——如果第一个同学转学了（删除列表头部元素），后面所有同学的座位号都往前挪了一位，老师会把二号座位的同学当成原来的一号同学（状态错乱）。用唯一 ID 做 key 就像用学号点名——不管座位怎么换，学号永远不变，老师能准确识别每个学生，不会张冠李戴。

**关键区别：**

| 场景 | index 做 key | 唯一 ID 做 key |
| - | - | - |
| 列表末尾增删 | ✅ 正常复用 | ✅ 正常复用 |
| 列表中间增删/重排 | ❌ 节点强制更新，状态错乱 | ✅ 精准复用，状态正确 |
| 性能 | 不必要的 DOM 更新 | 最小化 DOM 操作 |

> [!TIP]
>
> 永远用数据的唯一标识（id、uuid）做 key。如果没有唯一标识，宁可生成一个，也绝对不要用 index。`v-for="(item, index) in list"` 中的 index 只是遍历的下标，不代表节点的身份。

***

## 底层源码：Vue 3 的 diff 是如何实现的？

Vue 3 的 diff 核心函数在哪里？

Vue 3 的 diff 实现在 `patchKeyedChildren` 函数中（详见 [renderer.ts#L1794](https://github.com/vuejs/core/blob/main/packages/runtime-core/src/renderer.ts#L1794)）：

```typescript theme={null}
// packages/runtime-core/src/renderer.ts（极度简化）
const patchKeyedChildren = (
  c1: VNode[], // 旧子节点
  c2: VNode[], // 新子节点
  container: RendererElement,
) => {
  let i = 0;
  let e1 = c1.length - 1;
  let e2 = c2.length - 1;

  // 1. 从左到右：处理头部相同的部分
  while (i <= e1 && i <= e2 && isSameVNode(c1[i], c2[i])) {
    patch(c1[i], c2[i], container);
    i++;
  }
  // 2. 从右到左：处理尾部相同的部分
  while (i <= e1 && i <= e2 && isSameVNode(c1[e1], c2[e2])) {
    patch(c1[e1], c2[e2], container);
    e1--;
    e2--;
  }
  // 3. 中间乱序部分：建立映射 + LIS 优化
  // ...（详见源码）
};
```

这三步设计的精妙之处：前两步用 O(min(e1, e2)) 的时间处理了最常见的"头部/尾部新增删除"场景，只有真正乱序的部分才进入第三步的 LIS 计算。

LIS 算法是怎么实现的？

`getSequence` 与 `patchKeyedChildren` 同在 `renderer.ts` 中，详见 [renderer.ts](https://github.com/vuejs/core/blob/main/packages/runtime-core/src/renderer.ts)：

```typescript theme={null}
// packages/runtime-core/src/renderer.ts 底部的 getSequence
// 最长递增子序列：动态规划 + 贪心 + 二分查找
// 时间复杂度：O(n log n)
// 返回值：LIS 的索引数组（不是值数组）

// 例：输入 [3, 1, 4, 2]
// 返回 [1, 3]（索引 1 和 3 对应的值 1 和 2 是递增的）
```

***

## 十、及格回答 vs 满分回答

### 问题："Vue 3 的 diff 算法是怎么工作的？"

**及格回答（60-70分）：**

> "Vue 3 用最长递增子序列优化 diff。用 key 建立映射，找出不需要移动的节点，只移动不在序列中的节点。"

问题出在：**知道结论但不理解设计**。

**满分回答（90-100分）：**

> Vue 3 的 diff 算法在 `patchKeyedChildren` 函数中实现（位于 `packages/runtime-core/src/renderer.ts`），分三步走。第一步从左到右逐一比较，处理最常见的"只有内容更新、位置没变"的场景；第二步从右到左，处理尾部新增/删除的场景。这两步用 O(min) 时间覆盖了 80% 的真实场景——因为大多数列表更新都只是尾部增删或中间内容修改，节点位置不变。只有当这两步都无法处理时，才进入第三步的复杂逻辑。<sup>\[1]</sup>

> 第三步是中间乱序部分的核心优化：用 `keyToNewIndexMap` 建立 key 到新索引的映射（O(1) 查找），遍历旧节点查找可复用的，构建 `newIndexToOldIndexMap` 数组。然后用动态规划 + 贪心计算最长递增子序列（LIS），LIS 中的节点在旧列表中也是递增的，说明相对位置没变，不需要移动 DOM。这个算法在 `renderer.ts` 的 `getSequence` 函数中实现，时间复杂度 O(n log n)，比 Vue 2 双端比较在最坏情况下的 O(n²) 更优，且随 n 增大差距会持续扩大。<sup>\[2]</sup>

> 除了 diff 本身，Vue 3 还有三个编译器层面的优化：PatchFlag（位掩码标记动态绑定类型，运行时只更新标记部分，而不是比较所有属性）、Block Tree（扁平化动态后代数组，跳过静态子树，只遍历有动态绑定的节点）、静态提升（不依赖响应式的 VNode 只创建一次，后续更新直接复用）。这三个优化让 Vue 3 在绝大多数场景下比手写 render 函数还快——因为手写 render 函数无法享受编译器的静态分析。<sup>\[3]</sup>

### 常见追问

> 题目来源于真实面经（字节/腾讯/阿里前端面试、牛客网、haizlin/fe-interview），按难度递进。

> [accordion] 为什么不能用 index 做 key?
>
> **答题要点：**
> 核心是 index 不能代表节点身份。当列表中间增删元素时，后续元素的 index 全部变化，框架认为所有节点都变了。
>
> **高分回答：**
> "用 index 做 key 时，删除列表中间的元素会导致后续所有元素的 key 依次前移。例如删掉 index=0 的元素，原来 index=1 的元素变成 index=0，Vue 认为 key=0 的节点还在但 props 变了，于是强制更新而不是复用。如果节点内部有状态（如输入框内容、组件内部 data），状态会错乱——用户在原来 index=1 的输入框里打的字，会跑到 index=0 的位置。
>
> 解决方案是用数据的唯一标识（id、uuid）做 key。这样删除元素时，只有被删节点的 key 消失，其他节点的 key 不变，Vue 能精准识别哪些节点需要删除、哪些需要复用。"

> [accordion] Vue 3 的静态提升具体是怎么做的?
>
> **答题要点：**
> 区分"节点静态提升"和"事件缓存"两个层面。
>
> **高分回答：**
> "静态提升是编译器把不依赖响应式数据的 VNode 创建代码提升到渲染函数外部，只执行一次。例如 `<div><h1>Hello</h1><span>{{ msg }}</span></div>` 中，h1 节点完全静态，编译器会把它提升为模块级别的常量 `_hoisted_1`。后续每次渲染，这个 h1 的 VNode 直接复用引用，连对象创建都省了。
>
> 还有事件缓存：`@click="handler"` 在 Vue 3 中会被编译为 `_cache[0] || (_cache[0] = (...args) => handler(...args))`，确保内联函数不每次创建新的引用，避免子组件的 props 变化触发不必要的 diff。"

> [accordion] Block Tree 的动态节点追踪是怎么实现的?
>
> **答题要点：**
> Block 的核心是 `dynamicChildren` 数组——运行时把所有带 PatchFlag 的动态节点"收集"到一个扁平数组中。
>
> **高分回答：**
> "Block 是通过 `openBlock()` + `createBlock()` 实现的。在渲染函数执行时，`openBlock()` 初始化一个全局的 `currentBlock` 数组。当创建每个 VNode 时，如果它有 PatchFlag（说明是动态的），就会被 push 进 `currentBlock`。最后 `createBlock()` 把这个数组赋值给 Block VNode 的 `dynamicChildren`。
>
> 重渲染 diff 时，如果一个 VNode 有 `dynamicChildren`，就只遍历这个数组而不遍历 `children`。这意味着一棵有 1000 个静态节点 + 3 个动态节点的子树，diff 只比较 3 个节点。代价是 `v-if`/`v-for` 会创建新的子 Block，打断父 Block 的 `dynamicChildren` 收集，所以深层嵌套的条件渲染会削弱这个优化。"

> [accordion] Vue 3 的 diff 和 React 的 diff 有什么本质区别?
>
> **答题要点：**
> 从算法、编译优化、调度策略三个维度对比。
>
> **高分回答：**
> "算法层面，React 的 diff 用单端遍历 + key 映射（reconciliation），Vue 3 用前后缀同步 + LIS 优化。两者时间复杂度都是 O(n)，但 Vue 的前后缀同步在'头部/尾部增删'场景下更高效——React 即使只是尾部加一个元素，也要遍历整个列表。
>
> 编译优化是最大差异：Vue 3 的编译器能生成 PatchFlag、Block Tree、静态提升等优化代码，让运行时 diff 只比较动态部分。React 没有这个层次的编译优化——React Compiler（原 React Forget）只是自动添加 memo/useMemo，减少不必要的 re-render，但 diff 本身还是要全量比较 props。
>
> 调度策略上，Vue 3 用 queueJob + 微任务实现同步批量更新（同一事件循环内的多次修改只触发一次渲染）。React 18 用 Concurrent Mode 实现了时间切片和优先级调度（高优先级更新可以打断低优先级更新），在复杂场景下交互响应更流畅。"

> [accordion] 没有 key 时 Vue 是怎么 diff 的?
>
> **答题要点：** 没有 key 时 Vue 按索引逐一比较，可能导致状态错乱。
>
> **高分回答：**
> "没有 key 时，Vue 2 和 Vue 3 都会退化为按索引逐一比较。具体来说，如果旧列表有 3 个节点、新列表有 2 个节点，Vue 会：
>
> 1. 按索引 0、1 逐一比较旧节点和新节点，类型相同就 patch（更新 props）
> 2. 多出来的旧节点（索引 2）直接销毁
>
> 这种模式在列表末尾增删时没有问题，但在列表中间增删时会出错——因为索引变化了，Vue 认为同一个索引的节点还是同一个，只是 props 变了，于是强制更新而不是移动节点。这就是为什么 Vue 会给你一个 warning：'You are using v-for without key'。"

> [accordion] PatchFlag 的位运算具体是怎么做的?
>
> **答题要点：** 用按位或 (|) 组合标记，用按位与 (&) 检查标记。
>
> **高分回答：**
> "PatchFlag 是一个枚举值，用二进制位掩码表示。每个动态绑定类型占一个 bit：
>
> ```javascript theme={null}
> TEXT = 1; // 0000 0001
> CLASS = 1 << 1; // 0000 0010
> STYLE = 1 << 2; // 0000 0100
> PROPS = 1 << 3; // 0000 1000
> ```
>
> 编译器用按位或组合：`PatchFlag.CLASS | PatchFlag.STYLE` = `2 | 4` = `6`（二进制 `0000 0110`）。
> 运行时用按位与检查：`6 & PatchFlag.CLASS` = `6 & 2` = `2`（truthy，需要更新 class）。
> `6 & PatchFlag.TEXT` = `6 & 1` = `0`（falsy，跳过文本更新）。
>
> 位运算的性能极高——一条 CPU 指令就能完成，比遍历对象属性快几个数量级。"

> [accordion] v-if 和 v-for 对 Block Tree 有什么影响?
>
> **答题要点：** v-if/v-for 会创建子 Block，打断父 Block 的 dynamicChildren 收集。
>
> **高分回答：**
> "Block Tree 的核心是 `dynamicChildren`——运行时把所有带 PatchFlag 的动态节点收集到一个扁平数组中。但 `v-if` 和 `v-for` 会改变 DOM 结构（节点数量和层级不稳定），所以它们会创建自己的子 Block，打断父 Block 的 `dynamicChildren` 收集。
>
> 具体来说：当编译器遇到 `v-if` 时，会在 true/false 两个分支上各创建一个 Block。当遇到 `v-for` 时，会为列表的每一项创建一个 Block。这些子 Block 各自维护自己的 `dynamicChildren`，父 Block 只收集到子 Block 本身（作为一个整体），不再深入子 Block 内部收集动态节点。
>
> 这意味着深层嵌套的 `v-if`/`v-for` 会削弱 Block Tree 的优化效果——因为每个嵌套层级都会打断扁平化。"

> [accordion] 大型列表的 diff 性能优化有哪些实战方案?
>
> **答题要点：** 虚拟滚动 + key 优化 + 组件粒度控制。
>
> **高分回答：**
> "大型列表（1000+ 条）的 diff 性能优化有三个层面：
>
> 1）**虚拟滚动**：只渲染可视区域内的节点（通常 20-30 个），用 CSS transform 模拟滚动。库如 vue-virtual-scroller、@tanstack/vue-virtual。这是最根本的优化——把 1000 个节点的 diff 降到 30 个。
>
> 2）**key 优化**：用数据的唯一 ID 做 key，避免 index 做 key 导致的状态错乱和不必要的 DOM 更新。
>
> 3）**组件粒度控制**：把列表项封装为独立组件，配合 `shallowRef` 或 `v-memo`（Vue 3.2+）减少不必要的 re-render。`v-memo` 可以指定依赖项，只有依赖项变化时才重新渲染该节点。
>
> 4）**避免在 v-for 中使用 v-if**：Vue 3 中 `v-if` 的优先级高于 `v-for`，所以同一元素上的 `v-if` 访问不到 `v-for` 的作用域变量（`v-for` 优先是 Vue 2 的行为）。应避免在同一元素上同时使用二者，改用 `computed` 预先过滤列表，或用 `<template v-for>` 包裹。"

***

## Q4: 虚拟 DOM 真的比操作原生 DOM 快吗？

> [!TIP]
>
> 虚拟 DOM 就像自动驾驶——它不是最快的驾驶方式（顶级赛车手手动开肯定更快），但它足够安全、足够高效，而且你不需要成为赛车手就能上路。手写最优 DOM 操作就像职业赛车手手动驾驶——性能天花板更高，但对驾驶技术要求极高，普通开发者（大多数场景）用自动驾驶（虚拟 DOM）反而更安全、更高效。

> [!NOTE]
>
> Vue vs React 视角下的虚拟 DOM 性能对比，详见 [Vue vs React 架构对比](https://opc-43d279b8.mintlify.site/vue/vue-vs-react.md)。

### 答案：不是

这是一个经典的面试陷阱题。虚拟 DOM **不比直接操作原生 DOM 快**。如果你手写了最优的 DOM 操作代码，它一定比虚拟 DOM 快——因为虚拟 DOM 多了一层 JS 对象的创建和 diff 计算。

```text theme={null}
性能对比（从快到慢）：

1. 手写最优 DOM 操作（直接操作，无中间层）
2. 虚拟 DOM + diff（多了 JS 对象创建和比较）
3. 手写低效 DOM 操作（频繁重排、无批量更新）
```

### 那虚拟 DOM 的价值是什么？

虚拟 DOM 的价值不是"快"，而是**在保证足够好的性能的同时，提供了极大的开发体验提升**。

```javascript theme={null}
// 手写最优 DOM 操作（性能最好，但开发体验极差）
const list = document.getElementById('list')
// 你需要手动计算哪些 DOM 节点需要更新
// 手动判断是插入、删除还是移动
// 手动处理属性变化
// 手动处理事件绑定
// 代码量巨大，极易出错

// 虚拟 DOM（性能足够好，开发体验极佳）
// 你只需要声明数据状态
<ul>
  <li v-for="item in list" :key="item.id">{{ item.name }}</li>
</ul>
// 框架自动帮你计算最小 DOM 操作
```

***

## Q5: 虚拟 DOM 快在哪里？慢在哪里？

> [!TIP]
>
> 虚拟 DOM 的"快"就像你拿着购物清单去超市——你不会每拿一件商品就去收银台结一次账（批量更新），而是把所有商品放进购物车，最后一次性结算。它的"慢"就像你每次购物前都要先列清单、核对库存（创建 JS 对象 + diff 计算），这些额外步骤确实花了时间，但相比你一件件结账的低效，整体还是更省时。

### 快在哪里

**1. 批量更新**

```javascript theme={null}
// 手写 DOM：每次修改都触发重排
element.style.width = "100px"; // 重排
element.style.height = "100px"; // 重排
element.style.margin = "10px"; // 重排
// 3 次重排

// 虚拟 DOM：先在内存中计算，最后一次性更新
// diff 算法把 3 次修改合并为 1 次 DOM 操作
```

**2. 最小化 DOM 操作**

```javascript theme={null}
// 手写 DOM（低效）：全量替换
function updateList(newList) {
  container.innerHTML = "";
  newList.forEach((item) => {
    const li = document.createElement("li");
    li.textContent = item.name;
    container.appendChild(li);
  });
  // 销毁所有旧节点，创建所有新节点
}

// 虚拟 DOM：diff 计算最小变更
// 只更新变化的节点，复用不变的节点
```

**3. 跨平台能力**

```text theme={null}
虚拟 DOM 是 JS 对象，不依赖浏览器 API
→ 可以渲染到 Web、Native、Canvas、SSR
→ 一次编写，多端运行
```

### 慢在哪里

**1. JS 对象的创建开销**

```javascript theme={null}
// 虚拟 DOM 需要创建 VNode 对象
const vnode = {
  type: "div",
  props: { class: "container" },
  children: [
    { type: "h1", props: null, children: "Hello" },
    { type: "p", props: null, children: "World" },
  ],
};
// 每次渲染都要创建这些对象（虽然有静态提升优化）
```

**2. diff 算法的计算开销**

```javascript theme={null}
// diff 需要遍历新旧两棵 VNode 树
// 时间复杂度 O(n)，n 是节点数量
// 对于 1000 个节点的列表，需要比较 1000 次
```

**3. 内存占用**

```javascript theme={null}
// 虚拟 DOM 需要在内存中维护两棵树（新树和旧树）
// 对于大型应用，内存占用不可忽视
```

### 性能对比表

| 场景 | 手写最优 DOM | 虚拟 DOM | 手写低效 DOM |
| - | - | - | - |
| 简单更新 | 最快 | 略慢 | 慢 |
| 复杂列表 | 最快 | 较快 | 很慢 |
| 开发效率 | 极低 | 极高 | 低 |
| 维护成本 | 极高 | 低 | 高 |
| 出错概率 | 高 | 低 | 高 |

***

## Q6: 为什么不直接干掉虚拟 DOM？

> [!TIP]
>
> 虚拟 DOM 就像一个"通用翻译器"——它把你说的话（JSX/模板）翻译成一种中间语言（VNode），然后由不同的翻译官（渲染器）翻译成不同国家的语言（DOM、Native、SSR、Canvas）。如果去掉翻译器，你就得为每个国家单独学一门语言（为每个平台写不同的渲染代码）。声明式编程、组件化架构、跨平台渲染都需要这个"中间层"作为桥梁。

### 虚拟 DOM 的不可替代性

**1. 声明式编程的基石**

```javascript theme={null}
// 命令式（无虚拟 DOM）：告诉框架"怎么做"
const element = document.createElement('div')
element.textContent = message
element.addEventListener('click', handler)
container.appendChild(element)

// 声明式（有虚拟 DOM）：告诉框架"要什么"
<div @click="handler">{{ message }}</div>
```

声明式编程让开发者不需要关心 DOM 操作的细节，框架自动帮你计算最优的更新方式。

**2. 组件化架构的基础**

```javascript theme={null}
// 虚拟 DOM 让组件可以返回描述，而不是直接操作 DOM
function MyComponent(props) {
  return h("div", [h("h1", props.title), h("p", props.content)]);
}
// 组件只需要描述"要什么"，框架负责"怎么做"
```

**3. 跨平台渲染**

```text theme={null}
虚拟 DOM 是平台无关的 JS 对象
→ Web 渲染器：VNode → DOM
→ Native 渲染器：VNode → 原生组件
→ SSR 渲染器：VNode → HTML 字符串
→ Canvas 渲染器：VNode → Canvas 绘制指令
```

### 有没有比虚拟 DOM 更好的方案？

**Svelte 的编译时方案**

Svelte 不用虚拟 DOM，而是在编译时把组件转换为直接操作 DOM 的代码。

```javascript theme={null}
// Svelte 的组件
<script>
  let count = 0;
  function increment() { count += 1; }
</script>

<button on:click={increment}>
  Count: {count}
</button>

// 编译后（简化）
function create_fragment(ctx) {
  let button;
  return {
    create() {
      button = element("button");
      button.textContent = `Count: ${ctx.count}`;
    },
    update(ctx, [dirty]) {
      if (dirty & 1) {
        set_data(button, `Count: ${ctx.count}`);
      }
    },
    // ...
  }
}
```

Svelte 的优势是：没有运行时开销，bundle 更小。劣势是：大型应用的更新性能不如 Vue 3 的编译时优化（PatchFlag、Block Tree）。

**Vue 3 的编译时优化**

Vue 3 虽然保留了虚拟 DOM，但通过编译时优化大幅减少了运行时开销：

```text theme={null}
Vue 3 的三层优化：
1. PatchFlag：只比较有标记的动态属性
2. Block Tree：跳过整棵静态子树
3. 静态提升：静态 VNode 只创建一次

效果：Vue 3 的虚拟 DOM 在大多数场景下接近甚至超过手写 DOM 操作
```

### 结论

```text theme={null}
虚拟 DOM 不会被干掉，因为：
1. 声明式编程需要它（开发者不想手写 DOM 操作）
2. 组件化架构需要它（组件需要返回描述而不是直接操作 DOM）
3. 跨平台渲染需要它（平台无关的中间表示）
4. 编译时优化让它足够快（PatchFlag + Block Tree + 静态提升）

但虚拟 DOM 也在进化：
- Vue 3：编译时优化减少运行时开销
- Svelte：完全去掉虚拟 DOM，编译时生成 DOM 操作代码
- Solid.js：细粒度响应式 + 无虚拟 DOM
```

> [!TIP]
>
> 回答"虚拟 DOM 比原生 DOM 快吗"时，先说"不是"，然后解释虚拟 DOM 的价值不是"快"而是"开发体验 + 足够好的性能"。最后提一句 Vue 3 的编译时优化让它接近手写 DOM 的性能。这个回答展示了你对框架设计的深入理解。

***

## 延伸阅读与引用参考

* [Vue 3 源码：renderer.ts - patchKeyedChildren](https://github.com/vuejs/core/blob/main/packages/runtime-core/src/renderer.ts#L1794)：diff 算法的完整实现，从前缀/后缀同步到 LIS 优化的每一步。<sup>\[1]</sup>
* [Vue 3 源码：renderer.ts - getSequence](https://github.com/vuejs/core/blob/main/packages/runtime-core/src/renderer.ts)：动态规划 + 贪心 + 二分查找的经典实现。<sup>\[2]</sup>
* [Vue 3 官方文档：渲染机制](https://vuejs.org/guide/extras/rendering-mechanism.html)：尤雨溪对 PatchFlag、Block Tree、静态提升等编译器优化的最权威解释。<sup>\[3]</sup>
* [霍春阳《Vue.js 设计与实现》](https://book.douban.com/subject/35768338/)：从虚拟 DOM 到 diff 算法的完整实现剖析，包含 LIS 算法的详细推导。
* [刘博文《深入浅出 Vue.js》](https://book.douban.com/subject/32581255/)：从 Vue 2 时代开始的源码级解析，对 patch 和 diff 的演进有完整覆盖。


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.