CodeL
以前端为翼,以 AI 为脑,向全栈而行
2026-03-31

Vue 虚拟 DOM 与 Diff 算法完全指南

Vue 虚拟 DOM 与 Diff 算法完全指南 从"为什么需要虚拟 DOM"到"Diff 算法如何工作",深入理解 Vue 渲染机制的核心原理 目录 1. 核心概念 2. 虚拟 DOM 详解 3. Diff 算法原理...

Vue 虚拟 DOM 与 Diff 算法完全指南 #

从"为什么需要虚拟 DOM"到"Diff 算法如何工作",深入理解 Vue 渲染机制的核心原理


目录 #

  1. 核心概念
  2. 虚拟 DOM 详解
  3. Diff 算法原理
  4. Vue2 与 Vue3 的 Diff 差异
  5. Key 的重要性
  6. 性能优化实践
  7. 源码解析
  8. 常见问题
  9. 总结速记

一、核心概念 #

1.1 什么是虚拟 DOM?(大白话) #

虚拟 DOM(Virtual DOM) 就是用 JavaScript 对象来描述真实的 DOM 结构。

打个比方:

  • 真实 DOM = 一栋真实的房子(砖头、水泥、钢筋)
  • 虚拟 DOM = 房子的设计图纸(用数据描述房子结构)
// 真实 DOM
<div id="app" class="container">
  <h1>标题</h1>
  <p>内容</p>
</div>
 
// 虚拟 DOM(JavaScript 对象)
const vnode = {
  tag: 'div',
  props: {
    id: 'app',
    className: 'container'
  },
  children: [
    {
      tag: 'h1',
      props: {},
      children: ['标题']
    },
    {
      tag: 'p',
      props: {},
      children: ['内容']
    }
  ]
}

核心思想: 不直接操作真实 DOM,而是操作 JavaScript 对象(虚拟 DOM),然后让框架帮我们把变化"翻译"成真实 DOM 操作。

1.2 为什么需要虚拟 DOM? #

问题 1:直接操作 DOM 很慢

// 每次修改 DOM,浏览器都要:
// 1. 重新计算样式
// 2. 重新布局(reflow)
// 3. 重新绘制(repaint)
// 这些操作很昂贵!
 
// 糟糕的写法:频繁操作 DOM
for (let i = 0; i < 1000; i++) {
  const div = document.createElement('div');
  document.body.appendChild(div);  // 每次都触发 reflow
}
 
// 好的写法:批量更新
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
  const div = document.createElement('div');
  fragment.appendChild(div);  // 先加到文档片段,不触发 reflow
}
document.body.appendChild(fragment);  // 一次性添加

问题 2:手动优化 DOM 很难

// 需求:列表数据从 [A, B, C] 变成 [A, C, B]
 
// 方案 1:简单粗暴,清空重建
container.innerHTML = '';
[A, C, B].forEach(item => {
  container.appendChild(createElement(item));
});
// 问题:全部重建,性能差
 
// 方案 2:聪明一点,只移动需要的
// 移动 B 到 C 后面
container.insertBefore(container.children[1], container.children[2].nextSibling);
// 问题:代码复杂,容易出错,难维护
 
// 虚拟 DOM:自动帮你算出最优方案
// 只需要:newVnode = render([A, C, B])
// 框架自动:找到最小操作 = 移动 B

虚拟 DOM 解决的问题:

问题 直接操作 DOM 虚拟 DOM
性能 频繁操作,每次都触发 reflow 批量更新,最小化 DOM 操作
心智负担 手动优化,代码复杂 声明式,框架自动优化
跨平台 只能在浏览器 可以渲染到原生、Canvas、SVG
开发效率 命令式,关注"怎么做" 声明式,关注"是什么"

1.3 什么是 Diff 算法? #

Diff 算法 = 比较新旧虚拟 DOM,找出最小变化量的算法。

旧虚拟 DOM          新虚拟 DOM           Diff 结果
<div>               <div>               ┌─────────────────┐
  <span>A</span>      <span>A</span>     │ 保持不变        │
  <span>B</span>      <span>C</span>     │ 修改 B → C      │
  <span>C</span>      <span>D</span>     │ 修改 C → D      │
</div>              </div>              └─────────────────┘
 
Diff 算法目标:用最少的操作,把旧的变成新的

核心问题: 如何高效地找到最优解?

  • 朴素算法:O(n³) 复杂度(每个节点都和所有节点比较)
  • Vue 的算法:O(n) 复杂度(通过策略降低复杂度)

1.4 核心名词解释 #

名词 大白话解释
VNode Virtual Node,虚拟节点,一个 JS 对象描述一个 DOM 节点
Patch 补丁,Diff 算法计算出的差异,用于更新真实 DOM
Reconciliation 协调,React 中称为 Reconciler,即 Diff 过程
Same VNode 相同节点,tag 和 key 都相同的节点
Unmount 卸载,移除 DOM 节点,触发 beforeUnmount/unmounted 钩子
Patch Flag 补丁标记,Vue3 的优化,标记动态内容类型

二、虚拟 DOM 详解 #

2.1 虚拟 DOM 的结构 #

Vue2 的 VNode 结构:

// Vue2 源码简化版
function VNode(tag, data, children, text, elm) {
  this.tag = tag;           // 标签名:'div'、'span'、组件构造函数
  this.data = data;         // 属性:class、style、props、attrs、on...
  this.children = children; // 子节点数组
  this.text = text;         // 文本内容
  this.elm = elm;           // 对应的真实 DOM 节点
  this.key = data?.key;     // key,用于 Diff 优化
  // ... 其他属性
}
 
// 示例
const vnode = new VNode(
  'div',
  { class: 'container', key: 'app' },
  [
    new VNode('h1', {}, [new VNode(undefined, undefined, undefined, '标题')]),
    new VNode('p', {}, [new VNode(undefined, undefined, undefined, '内容')])
  ]
);

Vue3 的 VNode 结构:

// Vue3 源码简化版
function createVNode(type, props, children) {
  const vnode = {
    __v_isVNode: true,      // 标识是 VNode
    type,                    // 标签名或组件
    props,                   // 属性
    children,                // 子节点
    key: props?.key ?? null, // key
    ref: props?.ref ?? null, // ref
    
    // Vue3 新增:优化相关
    shapeFlag: 0,           // 形状标记(元素/组件/插槽...)
    patchFlag: 0,           // 补丁标记(动态内容类型)
    dynamicProps: null,     // 动态属性
    dynamicChildren: null,  // 动态子节点(Block 树)
    
    // 真实 DOM 引用
    el: null,
    anchor: null,
    target: null,
    targetAnchor: null,
  };
  
  // 设置 shapeFlag
  if (typeof type === 'string') {
    vnode.shapeFlag = ShapeFlags.ELEMENT;
  } else if (typeof type === 'function') {
    vnode.shapeFlag = ShapeFlags.FUNCTIONAL_COMPONENT;
  } else {
    vnode.shapeFlag = ShapeFlags.STATEFUL_COMPONENT;
  }
  
  // 设置 children
  if (children) {
    if (Array.isArray(children)) {
      vnode.shapeFlag |= ShapeFlags.ARRAY_CHILDREN;
    } else if (typeof children === 'string') {
      vnode.shapeFlag |= ShapeFlags.TEXT_CHILDREN;
    }
  }
  
  return vnode;
}
 
// 示例
const vnode = createVNode('div', { class: 'container' }, [
  createVNode('h1', null, '标题'),
  createVNode('p', null, '内容')
]);

2.2 虚拟 DOM 的创建过程 #

从模板到虚拟 DOM:

1. 模板编译

2. 生成渲染函数(render function)

3. 执行渲染函数,创建虚拟 DOM

4. 虚拟 DOM 转换为真实 DOM

代码示例:

<!-- 模板 -->
<template>
  <div class="container">
    <h1>{{ title }}</h1>
    <p>{{ content }}</p>
  </div>
</template>
 
<script>
export default {
  data() {
    return {
      title: '标题',
      content: '内容'
    }
  }
}
</script>

编译后生成的渲染函数:

// Vue2 编译结果(简化版)
function render() {
  with(this) {
    return _c('div', { staticClass: 'container' }, [
      _c('h1', [_v(_s(title))]),
      _c('p', [_v(_s(content))])
    ])
  }
}
 
// Vue3 编译结果(简化版)
function render(_ctx, _cache) {
  return _createElementVNode('div', { class: 'container' }, [
    _createElementVNode('h1', null, _toDisplayString(_ctx.title), 1 /* TEXT */),
    _createElementVNode('p', null, _toDisplayString(_ctx.content), 1 /* TEXT */)
  ])
}

2.3 虚拟 DOM 的优势与代价 #

优势:

优势 说明
跨平台 虚拟 DOM 是纯 JS 对象,可以渲染到任何平台
批量更新 多次修改合并成一次 DOM 操作
最小化操作 Diff 算法找出最小变化
组件化 每个组件对应一棵虚拟 DOM 树

代价:

代价 说明
内存占用 需要维护虚拟 DOM 树
计算开销 每次更新都要创建新的虚拟 DOM 并 Diff
初次渲染 比直接 innerHTML 慢

什么时候虚拟 DOM 比直接 DOM 操作快?

// 场景 1:大量数据变化
// 直接 DOM 操作:需要遍历每个元素,逐个修改
// 虚拟 DOM:批量 Diff 后一次性更新
// ✅ 虚拟 DOM 更快
 
// 场景 2:单次简单修改
// 直接 DOM 操作:element.textContent = 'new text'
// 虚拟 DOM:创建 VNode → Diff → Patch
// ❌ 直接 DOM 操作更快
 
// 结论:
// - 大量更新、复杂 UI → 虚拟 DOM 有优势
// - 少量更新、简单 UI → 直接 DOM 操作可能更快
// - 但虚拟 DOM 的心智模型更好,开发效率更高

三、Diff 算法原理 #

3.1 Diff 算法的核心策略 #

Vue 的 Diff 算法采用"同层比较"策略:

只比较同一层级的节点,不跨层级比较
 
        A                    A
       / \                  / \
      B   C       →        B   C
     /     \              /     \
    D       E            D'      E'
    
只比较:
- A vs A
- B vs B
- C vs C
- D vs D'
- E vs E'
 
不会比较:
- D vs C
- B vs E

为什么可以这样做?

  1. 实际情况:跨层级移动节点的场景很少
  2. 复杂度降低:O(n³) → O(n)
  3. 实现简单:递归比较即可

3.2 Diff 算法的整体流程 #

┌─────────────────────────────────────────────────────────────┐
│                    Vue Diff 算法流程                         │
└─────────────────────────────────────────────────────────────┘
 
1. 比较新旧节点是否相同
   ├── 相同:patchVnode(更新属性、处理子节点)
   └── 不同:替换整个节点
 
2. 如果节点相同,处理子节点
   ├── 新旧都有子节点:updateChildren(核心 Diff)
   ├── 只有新节点有子节点:添加子节点
   ├── 只有旧节点有子节点:删除子节点
   └── 新旧都无子节点:只更新属性
 
3. updateChildren(核心算法)
   ├── 双端比较(Vue2)
   └── 最长递增子序列(Vue3)

3.3 Vue2 的双端 Diff 算法 #

双端比较:同时从两端开始比较

// Vue2 源码简化版
function updateChildren(parentElm, oldCh, newCh) {
  let oldStartIdx = 0;
  let oldEndIdx = oldCh.length - 1;
  let newStartIdx = 0;
  let newEndIdx = newCh.length - 1;
  
  let oldStartVnode = oldCh[oldStartIdx];
  let oldEndVnode = oldCh[oldEndIdx];
  let newStartVnode = newCh[newStartIdx];
  let newEndVnode = newCh[newEndIdx];
  
  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    if (sameVnode(oldStartVnode, newStartVnode)) {
      // 头对头:新旧头部节点相同
      patchVnode(oldStartVnode, newStartVnode);
      oldStartVnode = oldCh[++oldStartIdx];
      newStartVnode = newCh[++newStartIdx];
    } else if (sameVnode(oldEndVnode, newEndVnode)) {
      // 尾对尾:新旧尾部节点相同
      patchVnode(oldEndVnode, newEndVnode);
      oldEndVnode = oldCh[--oldEndIdx];
      newEndVnode = newCh[--newEndIdx];
    } else if (sameVnode(oldStartVnode, newEndVnode)) {
      // 头对尾:旧头和新尾相同
      patchVnode(oldStartVnode, newEndVnode);
      // 移动到末尾
      parentElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling);
      oldStartVnode = oldCh[++oldStartIdx];
      newEndVnode = newCh[--newEndIdx];
    } else if (sameVnode(oldEndVnode, newStartVnode)) {
      // 尾对头:旧尾和新头相同
      patchVnode(oldEndVnode, newStartVnode);
      // 移动到开头
      parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm);
      oldEndVnode = oldCh[--oldEndIdx];
      newStartVnode = newCh[++newStartIdx];
    } else {
      // 四种情况都不匹配,用 key 查找
      let idxInOld = findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx);
      if (idxInOld) {
        // 找到了,移动过来
        const vnodeToMove = oldCh[idxInOld];
        patchVnode(vnodeToMove, newStartVnode);
        oldCh[idxInOld] = undefined;
        parentElm.insertBefore(vnodeToMove.elm, oldStartVnode.elm);
      } else {
        // 没找到,创建新节点
        createElm(newStartVnode, parentElm, oldStartVnode.elm);
      }
      newStartVnode = newCh[++newStartIdx];
    }
  }
  
  // 处理剩余节点
  if (oldStartIdx > oldEndIdx) {
    // 旧的遍历完了,新的还有,批量添加
    const refElm = newCh[newEndIdx + 1] ? newCh[newEndIdx + 1].elm : null;
    addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx);
  } else if (newStartIdx > newEndIdx) {
    // 新的遍历完了,旧的还有,批量删除
    removeVnodes(oldCh, oldStartIdx, oldEndIdx);
  }
}

双端比较的四种情况图解:

旧节点:[A, B, C, D]
新节点:[D, A, B, C]
 
第 1 步:比较旧头 A 和新头 D
        不匹配
 
第 2 步:比较旧尾 D 和新尾 C
        不匹配
 
第 3 步:比较旧头 A 和新尾 C
        不匹配
 
第 4 步:比较旧尾 D 和新头 D
        匹配!移动 D 到开头
        旧:[A, B, C, _]
        新:[D, A, B, C]
        
继续比较...
最终:D 移到开头,其他不变

3.4 Vue3 的最长递增子序列 Diff #

Vue3 优化了 Diff 算法,使用最长递增子序列(LIS):

// Vue3 源码简化版
function updateChildren(parent, oldChildren, newChildren) {
  let i = 0;
  const newChildrenLength = newChildren.length;
  let oldChildrenLength = oldChildren.length;
  
  // 1. 从头部开始同步
  while (i <= oldChildrenLength && i <= newChildrenLength) {
    const oldChild = oldChildren[i];
    const newChild = newChildren[i];
    
    if (!isSameVNodeType(oldChild, newChild)) {
      break;
    }
    
    patch(oldChild, newChild);
    i++;
  }
  
  // 2. 从尾部开始同步
  while (i <= oldChildrenLength && i <= newChildrenLength) {
    const oldChild = oldChildren[oldChildrenLength];
    const newChild = newChildren[newChildrenLength];
    
    if (!isSameVNodeType(oldChild, newChild)) {
      break;
    }
    
    patch(oldChild, newChild);
    oldChildrenLength--;
    newChildrenLength--;
  }
  
  // 3. 处理剩余的新节点(新增)
  if (i > oldChildrenLength) {
    if (i <= newChildrenLength) {
      // 批量添加新节点
      const nextPos = newChildrenLength + 1;
      const anchor = nextPos < newChildren.length ? newChildren[nextPos].el : null;
      while (i <= newChildrenLength) {
        mount(newChildren[i++], parent, anchor);
      }
    }
  }
  
  // 4. 处理剩余的旧节点(删除)
  else if (i > newChildrenLength) {
    while (i <= oldChildrenLength) {
      unmount(oldChildren[i++]);
    }
  }
  
  // 5. 处理乱序部分(核心:最长递增子序列)
  else {
    const oldStartIdx = i;
    const newStartIdx = i;
    const oldEndIdx = oldChildrenLength;
    const newEndIdx = newChildrenLength;
    
    // 5.1 建立 key -> index 映射
    const keyToNewIndexMap = new Map();
    for (i = newStartIdx; i <= newEndIdx; i++) {
      keyToNewIndexMap.set(newChildren[i].key, i);
    }
    
    // 5.2 遍历旧节点,更新能匹配的,标记要删除的
    const toBePatched = newEndIdx - newStartIdx + 1;
    const newIndexToOldIndexMap = new Array(toBePatched).fill(0);
    
    let moved = false;
    let maxNewIndexSoFar = 0;
    
    for (i = oldStartIdx; i <= oldEndIdx; i++) {
      const oldChild = oldChildren[i];
      const newIndex = keyToNewIndexMap.get(oldChild.key);
      
      if (newIndex === undefined) {
        // 旧节点不在新列表中,删除
        unmount(oldChild);
      } else {
        // 更新映射
        newIndexToOldIndexMap[newIndex - newStartIdx] = i + 1;
        
        // 判断是否需要移动
        if (newIndex >= maxNewIndexSoFar) {
          maxNewIndexSoFar = newIndex;
        } else {
          moved = true;
        }
        
        patch(oldChild, newChildren[newIndex]);
      }
    }
    
    // 5.3 移动和挂载
    const increasingNewIndexSequence = moved
      ? getSequence(newIndexToOldIndexMap)
      : [];
    
    let j = increasingNewIndexSequence.length - 1;
    
    for (i = toBePatched - 1; i >= 0; i--) {
      const nextIndex = newStartIdx + i;
      const newChild = newChildren[nextIndex];
      const anchor = nextIndex + 1 < newChildren.length ? newChildren[nextIndex + 1].el : null;
      
      if (newIndexToOldIndexMap[i] === 0) {
        // 新节点,挂载
        mount(newChild, parent, anchor);
      } else if (moved) {
        // 需要移动
        if (j < 0 || i !== increasingNewIndexSequence[j]) {
          move(newChild, parent, anchor);
        } else {
          j--;
        }
      }
    }
  }
}
 
// 最长递增子序列算法
function getSequence(arr) {
  const p = arr.slice();
  const result = [0];
  let i, j, u, v, c;
  const len = arr.length;
  
  for (i = 0; i < len; i++) {
    const arrI = arr[i];
    if (arrI !== 0) {
      j = result[result.length - 1];
      if (arr[j] < arrI) {
        p[i] = j;
        result.push(i);
        continue;
      }
      
      u = 0;
      v = result.length - 1;
      while (u < v) {
        c = (u + v) >> 1;
        if (arr[result[c]] < arrI) {
          u = c + 1;
        } else {
          v = c;
        }
      }
      
      if (arrI < arr[result[u]]) {
        if (u > 0) {
          p[i] = result[u - 1];
        }
        result[u] = i;
      }
    }
  }
  
  u = result.length;
  v = result[u - 1];
  while (u-- > 0) {
    result[u] = v;
    v = p[v];
  }
  
  return result;
}

最长递增子序列图解:

旧节点:[A, B, C, D, E]
新节点:[A, C, D, B, E]
 
1. 建立 key -> index 映射(新节点)
   { A: 0, C: 1, D: 2, B: 3, E: 4 }
 
2. 遍历旧节点,建立 newIndexToOldIndexMap
   旧节点索引:0(A), 1(B), 2(C), 3(D), 4(E)
   新节点索引:0(A), 3(B), 1(C), 2(D), 4(E)
   
   newIndexToOldIndexMap = [1, 3, 4, 2, 5]
   // 含义:新位置 0 的元素在旧位置 1
   //       新位置 1 的元素在旧位置 3
   //       ...
 
3. 计算最长递增子序列
   [1, 3, 4, 2, 5] 的 LIS = [1, 2, 5]
   对应新索引:[0, 2, 4] = [A, D, E]
   
   含义:A、D、E 不需要移动
   只有 C 和 B 需要移动
 
4. 移动节点
   C 移到 B 前面
   B 移到 D 前面
   
最终结果:[A, C, D, B, E]

3.5 Vue2 vs Vue3 Diff 算法对比 #

特性 Vue2 Vue3
算法 双端比较 最长递增子序列
复杂度 O(n) 平均 O(n log n) 最坏情况
优势 简单直观 移动次数更少
劣势 某些场景移动次数多 实现复杂
示例对比:
 
旧:[A, B, C, D, E, F, G]
新:[A, C, D, B, E, F, G]
 
Vue2 双端比较:
- 头对头:A = A ✓
- 尾对尾:G = G ✓
- 头对尾:B ≠ G
- 尾对头:G ≠ C
- 查找:找到 C,移动
- 继续比较...
- 最终:移动多次
 
Vue3 LIS:
- 前缀同步:A
- 后缀同步:E, F, G
- 中间:[B, C, D] → [C, D, B]
- LIS:[C, D]
- 只需移动 B
- 最终:只移动 1 次

四、Vue2 与 Vue3 的 Diff 差异 #

4.1 架构层面的差异 #

Vue2:

Options API

模板编译

渲染函数(render)

虚拟 DOM(VNode)

Diff(双端比较)

Patch(更新真实 DOM)

Vue3:

Composition API

模板编译(带 Patch Flag)

渲染函数(带优化标记)

虚拟 DOM(Block Tree)

Diff(最长递增子序列)

Patch(靶向更新)

4.2 Patch Flag 优化 #

Vue3 引入 Patch Flag,实现靶向更新:

// Vue2:全量 Diff
<div class="container">
  <h1>{{ title }}</h1>
  <p>{{ content }}</p>
</div>
 
// Vue3:带 Patch Flag
// 编译结果
function render(_ctx, _cache) {
  return _createElementVNode('div', { class: 'container' }, [
    _createElementVNode('h1', null, _toDisplayString(_ctx.title), 1 /* TEXT */),
    _createElementVNode('p', null, _toDisplayString(_ctx.content), 1 /* TEXT */)
  ])
}
 
// Patch Flag 含义
const PatchFlags = {
  TEXT: 1,           // 动态文本
  CLASS: 2,          // 动态 class
  STYLE: 4,          // 动态 style
  PROPS: 8,          // 动态属性
  FULL_PROPS: 16,    // 有动态 key 的属性
  HYDRATE_EVENTS: 32,// 有事件监听器
  STABLE_FRAGMENT: 64,
  KEYED_FRAGMENT: 128,
  UNKEYED_FRAGMENT: 256,
  NEED_PATCH: 512,
  DYNAMIC_SLOTS: 1024,
  HOISTED: -1,
  BAIL: -2
};

好处:

// 只更新标记为动态的部分
// 静态内容不会参与 Diff
 
<div>
  <span class="static">静态内容</span>  <!-- 不会比较 -->
  <span class="dynamic">{{ title }}</span>  <!-- 只比较这个 -->
</div>

4.3 Block Tree 优化 #

Vue3 的 Block Tree:

// Vue2:整棵树参与 Diff
<div>
  <div>
    <span>静态</span>
    <span>{{ title }}</span>
  </div>
  <div>
    <span>静态</span>
    <span>{{ content }}</span>
  </div>
</div>
 
// Vue3:收集动态节点到 Block
// 只有动态节点参与 Diff
const block = {
  dynamicChildren: [
    vnode1,  // {{ title }}
    vnode2,  // {{ content }}
  ]
};

Block 的收集过程:

// 模板
<div>
  <span>静态</span>
  <span>{{ title }}</span>
</div>
 
// 编译后
function render() {
  return (_openBlock(), _createElementBlock('div', null, [
    _createElementVNode('span', null, '静态'),  // 静态,不收集
    _createElementVNode('span', null, _toDisplayString(title), 1 /* TEXT */)  // 动态,收集
  ]));
}
 
// Block 收集动态子节点
// 只有带 Patch Flag 的节点会被收集

4.4 静态提升 #

Vue3 会提升静态节点:

// Vue2:每次渲染都创建 VNode
function render() {
  return _c('div', [
    _c('span', { staticClass: 'title' }, '静态标题'),  // 每次都创建
    _c('span', [_v(_s(title))])
  ]);
}
 
// Vue3:静态节点提升到渲染函数外
const _hoisted_1 = _createElementVNode('span', { class: 'title' }, '静态标题');
 
function render() {
  return (_openBlock(), _createElementBlock('div', null, [
    _hoisted_1,  // 直接复用
    _createElementVNode('span', null, _toDisplayString(title), 1)
  ]));
}

好处:

  • 静态节点只创建一次
  • 减少内存占用
  • 减少创建 VNode 的开销

4.5 缓存事件处理函数 #

Vue3 会缓存内联事件:

// Vue2:每次渲染创建新函数
function render() {
  return _c('button', {
    on: {
      click: function($event) { _ctx.count++ }  // 每次创建
    }
  }, '点击');
}
 
// Vue3:缓存事件处理函数
function render() {
  return (_openBlock(), _createElementBlock('button', {
    onClick: _cache[0] || (_cache[0] = ($event) => _ctx.count++)  // 缓存
  }, '点击'));
}

好处:

  • 避免不必要的更新(子组件认为 props 没变)
  • 减少函数创建开销

五、Key 的重要性 #

5.1 Key 的作用 #

Key 是 VNode 的唯一标识,用于 Diff 算法判断节点是否相同。

// 判断两个节点是否相同(sameVnode)
function sameVnode(vnode1, vnode2) {
  return (
    vnode1.key === vnode2.key &&  // key 相同
    vnode1.tag === vnode2.tag     // 标签相同
  );
}

没有 Key 会怎样?

// 旧列表
[A, B, C]
 
// 新列表(删除 B)
[A, C]
 
// 没有 Key 时:
// Vue 认为 A=A,但 B≠C
// 所以会:保留 A,修改 B 为 C,删除 C
// 操作:1 次修改 + 1 次删除
 
// 有 Key 时:
// Vue 知道 A=A,C=C,B 被删除
// 操作:1 次删除(删除 B)
// 更高效!

5.2 为什么不能用 index 作为 Key? #

错误示例:

<template>
  <ul>
    <!-- ❌ 不要用 index 作为 key -->
    <li v-for="(item, index) in list" :key="index">
      {{ item.name }}
    </li>
  </ul>
</template>

问题分析:

初始状态:
list = [{ id: 1, name: 'A' }, { id: 2, name: 'B' }]
index = [0, 1]
渲染:<li key="0">A</li><li key="1">B</li>
 
在开头插入新元素:
list = [{ id: 3, name: 'C' }, { id: 1, name: 'A' }, { id: 2, name: 'B' }]
index = [0, 1, 2]
 
用 index 作为 key:
<li key="0">C</li>  <!-- 之前是 A,现在变成了 C -->
<li key="1">A</li>  <!-- 之前是 B,现在变成了 A -->
<li key="2">B</li>  <!-- 新增 -->
 
Diff 结果:
- key="0" 的节点:内容从 A 变成 C(修改)
- key="1" 的节点:内容从 B 变成 A(修改)
- key="2" 的节点:新增 B(创建)
 
问题:本应只插入 1 个节点,现在修改了 2 个 + 新增了 1 个

正确做法:

<template>
  <ul>
    <!-- ✅ 用唯一 ID 作为 key -->
    <li v-for="item in list" :key="item.id">
      {{ item.name }}
    </li>
  </ul>
</template>

分析:

初始状态:
list = [{ id: 1, name: 'A' }, { id: 2, name: 'B' }]
渲染:<li key="1">A</li><li key="2">B</li>
 
在开头插入新元素:
list = [{ id: 3, name: 'C' }, { id: 1, name: 'A' }, { id: 2, name: 'B' }]
 
用 id 作为 key:
<li key="3">C</li>  <!-- 新增 -->
<li key="1">A</li>  <!-- 保持不变 -->
<li key="2">B</li>  <!-- 保持不变 -->
 
Diff 结果:
- key="3" 的节点:新增(创建)
- key="1" 的节点:复用(无修改)
- key="2" 的节点:复用(无修改)
 
完美:只新增了 1 个节点,其他复用

5.3 Key 的最佳实践 #

<template>
  <!-- ✅ 列表渲染:用唯一标识 -->
  <li v-for="item in items" :key="item.id">{{ item.name }}</li>
  
  <!-- ✅ 简单列表:没有唯一 ID,可以用 item 本身(如果唯一) -->
  <span v-for="tag in tags" :key="tag">{{ tag }}</span>
  
  <!-- ✅ 强制重新渲染:改变 key -->
  <UserForm :key="formKey" />
  <button @click="formKey++">重置表单</button>
  
  <!-- ❌ 不要用 index -->
  <li v-for="(item, index) in items" :key="index">...</li>
  
  <!-- ❌ 不要用随机数(每次渲染都变) -->
  <li v-for="item in items" :key="Math.random()">...</li>
  
  <!-- ❌ 不要省略 key(等同于用 index) -->
  <li v-for="item in items">...</li>
</template>

5.4 Key 的特殊情况 #

强制重新渲染:

<template>
  <!-- 通过改变 key,强制组件重新渲染 -->
  <UserForm :key="formKey" />
  
  <button @click="resetForm">重置表单</button>
</template>
 
<script>
export default {
  data() {
    return {
      formKey: 0
    };
  },
  methods: {
    resetForm() {
      // 改变 key,组件会被销毁重建
      this.formKey++;
    }
  }
};
</script>

v-if 和 v-for:

<template>
  <!-- ❌ 不推荐:v-if 和 v-for 同级 -->
  <li v-for="item in items" v-if="item.show" :key="item.id">
    {{ item.name }}
  </li>
  
  <!-- ✅ 推荐:使用 computed -->
  <li v-for="item in visibleItems" :key="item.id">
    {{ item.name }}
  </li>
  
  <!-- ✅ 或者用 template 包裹 -->
  <template v-for="item in items" :key="item.id">
    <li v-if="item.show">{{ item.name }}</li>
  </template>
</template>
 
<script>
export default {
  computed: {
    visibleItems() {
      return this.items.filter(item => item.show);
    }
  }
};
</script>

六、性能优化实践 #

6.1 减少 Diff 范围 #

使用 computed 缓存:

<template>
  <!-- ❌ 每次渲染都过滤 -->
  <li v-for="item in items.filter(i => i.active)" :key="item.id">
    {{ item.name }}
  </li>
  
  <!-- ✅ 用 computed 缓存 -->
  <li v-for="item in activeItems" :key="item.id">
    {{ item.name }}
  </li>
</template>
 
<script>
export default {
  computed: {
    activeItems() {
      return this.items.filter(i => i.active);
    }
  }
};
</script>

6.2 使用 v-once 和 v-memo #

v-once:只渲染一次

<template>
  <!-- 静态内容,只渲染一次,之后不再更新 -->
  <div v-once>
    <h1>静态标题</h1>
    <p>这段内容永远不会改变</p>
  </div>
  
  <!-- 初始渲染后,不再参与 Diff -->
  <MyComponent v-once :data="initialData" />
</template>

v-memo:条件缓存(Vue3.2+)

<template>
  <!-- 只有当 item.id 或 selected 变化时才更新 -->
  <div v-for="item in list" :key="item.id" v-memo="[item.id, selected]">
    <span :class="{ active: item.id === selected }">{{ item.name }}</span>
  </div>
  
  <!-- 性能优化:大列表 -->
  <tr v-for="row in hugeList" :key="row.id" v-memo="[row.id, sortKey]">
    <td>{{ row.name }}</td>
    <td>{{ row.value }}</td>
  </tr>
</template>

6.3 合理拆分组件 #

<!-- ❌ 大组件:任何变化都会触发整个组件 Diff -->
<template>
  <div>
    <Header />
    <Content :data="contentData" />
    <Footer />
  </div>
</template>
 
<!-- ✅ 拆分组件:只有变化的部分 Diff -->
<template>
  <div>
    <Header />  <!-- 独立组件,不受其他影响 -->
    <Content :data="contentData" />  <!-- 只有 contentData 变化时更新 -->
    <Footer />  <!-- 独立组件 -->
  </div>
</template>

6.4 避免不必要的响应式数据 #

// ❌ 不需要响应式的大数据
export default {
  data() {
    return {
      hugeList: []  // 响应式,每个元素都有 getter/setter
    };
  }
};
 
// ✅ 非响应式数据
export default {
  created() {
    this.hugeList = [];  // 普通属性,不是响应式
  }
};
 
// ✅ Vue3 shallowRef
import { shallowRef } from 'vue';
 
const hugeList = shallowRef([]);  // 只有 .value 是响应式,内部元素不是

6.5 虚拟列表 #

大列表使用虚拟滚动:

<template>
  <!-- 使用 vue-virtual-scroller 或类似库 -->
  <RecycleScroller
    :items="hugeList"
    :item-size="50"
    key-field="id"
    v-slot="{ item }"
  >
    <div class="item">{{ item.name }}</div>
  </RecycleScroller>
</template>
 
<script>
import { RecycleScroller } from 'vue-virtual-scroller';
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css';
 
export default {
  components: { RecycleScroller },
  data() {
    return {
      hugeList: Array.from({ length: 10000 }, (_, i) => ({
        id: i,
        name: `Item ${i}`
      }))
    };
  }
};
</script>

6.6 使用 DevTools 分析 #

// Vue DevTools 性能分析
// 1. 安装 Vue DevTools 浏览器扩展
// 2. 打开开发者工具,切换到 Vue 标签
// 3. 点击"Performance"选项卡
// 4. 点击录制,操作页面,停止录制
// 5. 查看每个组件的渲染时间
 
// 或使用 performance API
performance.mark('start');
// ... 执行代码
performance.mark('end');
performance.measure('MyComponent render', 'start', 'end');
 
const measures = performance.getEntriesByName('MyComponent render');
console.log('渲染时间:', measures[0].duration, 'ms');

七、源码解析 #

7.1 Vue2 patch 函数 #

// Vue2 源码简化版
function patch(oldVnode, vnode) {
  // 如果 oldVnode 不存在,创建新元素
  if (isUndef(oldVnode)) {
    createElm(vnode);
  } else {
    const isRealElement = isDef(oldVnode.nodeType);
    
    if (!isRealElement && sameVnode(oldVnode, vnode)) {
      // 相同节点,更新
      patchVnode(oldVnode, vnode);
    } else {
      // 不同节点,替换
      const oldElm = oldVnode.elm;
      const parentElm = nodeOps.parentNode(oldElm);
      
      // 创建新元素
      createElm(vnode, parentElm, nodeOps.nextSibling(oldElm));
      
      // 删除旧元素
      if (parentElm) {
        removeVnodes([oldVnode]);
      }
    }
  }
  
  return vnode.elm;
}
 
function patchVnode(oldVnode, vnode) {
  const elm = vnode.elm = oldVnode.elm;
  const oldCh = oldVnode.children;
  const ch = vnode.children;
  
  // 更新属性
  if (isDef(vnode.data)) {
    updateAttributes(oldVnode.data, vnode.data);
  }
  
  // 处理文本节点
  if (isUndef(oldCh) && isDef(ch)) {
    if (isDef(oldVnode.text)) {
      nodeOps.setTextContent(elm, '');
    }
    addVnodes(elm, null, ch);
  } else if (isDef(oldCh) && isUndef(ch)) {
    removeVnodes(oldCh);
  } else if (isDef(oldCh) && isDef(ch)) {
    // 都有子节点,Diff
    updateChildren(elm, oldCh, ch);
  } else if (isDef(oldVnode.text)) {
    nodeOps.setTextContent(elm, '');
  }
}

7.2 Vue3 patch 函数 #

// Vue3 源码简化版
function patch(
  n1,
  n2,
  container,
  anchor = null,
  parentComponent = null,
  parentSuspense = null,
  isSVG = false,
  slotScopeIds = null,
  optimized = false
) {
  // 如果新旧节点类型不同,卸载旧节点
  if (n1 && !isSameVNodeType(n1, n2)) {
    anchor = getNextHostNode(n1);
    unmount(n1, parentComponent, parentSuspense, true);
    n1 = null;
  }
  
  const { type, shapeFlag } = n2;
  
  switch (type) {
    case Text:
      processText(n1, n2, container, anchor);
      break;
    case Comment:
      processCommentNode(n1, n2, container, anchor);
      break;
    case Fragment:
      processFragment(n1, n2, container, anchor, parentComponent, parentSuspense, slotScopeIds, optimized);
      break;
    default:
      if (shapeFlag & ShapeFlags.ELEMENT) {
        // 处理原生元素
        processElement(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized);
      } else if (shapeFlag & ShapeFlags.COMPONENT) {
        // 处理组件
        processComponent(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized);
      } else if (shapeFlag & ShapeFlags.TELEPORT) {
        // 处理 Teleport
        type.process(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized);
      } else if (shapeFlag & ShapeFlags.SUSPENSE) {
        // 处理 Suspense
        type.process(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized);
      }
  }
}
 
function processElement(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized) {
  isSVG = isSVG || n2.type === 'svg';
  
  if (n1 == null) {
    // 挂载
    mountElement(n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized);
  } else {
    // 更新
    patchElement(n1, n2, parentComponent, parentSuspense, slotScopeIds, optimized);
  }
}
 
function patchElement(n1, n2, parentComponent, parentSuspense, slotScopeIds, optimized) {
  const el = (n2.el = n1.el);
  const oldProps = n1.props || {};
  const newProps = n2.props || {};
  
  // 更新子节点
  patchChildren(n1, n2, el, null, parentComponent, parentSuspense, slotScopeIds, optimized);
  
  // 更新属性
  patchProps(el, n2, oldProps, newProps, ...);
}

7.3 最长递增子序列算法 #

// Vue3 源码:最长递增子序列(LIS)
// 算法:贪心 + 二分查找
// 时间复杂度:O(n log n)
 
function getSequence(arr) {
  const p = arr.slice();  // 前驱节点数组
  const result = [0];     // 结果数组
  let i, j, u, v, c;
  const len = arr.length;
  
  for (i = 0; i < len; i++) {
    const arrI = arr[i];
    
    if (arrI !== 0) {
      j = result[result.length - 1];
      
      // 如果当前值大于结果最后一个值,直接追加
      if (arr[j] < arrI) {
        p[i] = j;
        result.push(i);
        continue;
      }
      
      // 二分查找,找到第一个大于等于 arrI 的位置
      u = 0;
      v = result.length - 1;
      
      while (u < v) {
        c = (u + v) >> 1;  // 中间位置
        
        if (arr[result[c]] < arrI) {
          u = c + 1;
        } else {
          v = c;
        }
      }
      
      // 替换找到的位置
      if (arrI < arr[result[u]]) {
        if (u > 0) {
          p[i] = result[u - 1];
        }
        result[u] = i;
      }
    }
  }
  
  // 回溯,得到正确的序列
  u = result.length;
  v = result[u - 1];
  
  while (u-- > 0) {
    result[u] = v;
    v = p[v];
  }
  
  return result;
}
 
// 示例
getSequence([2, 3, 1, 5, 6, 8, 7, 9, 4]);
// 返回:[2, 3, 5, 6, 7, 9] 或 [1, 3, 5, 6, 7, 9]
// 表示这些位置的元素组成最长递增子序列

八、常见问题 #

Q1:虚拟 DOM 一定比直接操作 DOM 快吗? #

答案:不一定。

场景 更快的方式
大量数据批量更新 虚拟 DOM
少量简单更新 直接 DOM
首次渲染 innerHTML
频繁小更新 虚拟 DOM(批量合并)

结论: 虚拟 DOM 的优势更多在于开发体验(声明式编程)和跨平台能力,而不是纯粹的性能。

Q2:为什么 Vue 的 Diff 算法是 O(n) 而不是 O(n³)? #

答案:因为采用了"同层比较"策略。

O(n³) 的朴素算法:
- 遍历旧树的所有节点
- 对于每个节点,遍历新树的所有节点
- 找到最匹配的节点
- 复杂度:n * n * n = O(n³)
 
O(n) 的 Vue 算法:
- 只比较同层级的节点
- 通过 key 快速匹配
- 双端比较或 LIS
- 复杂度:n = O(n)

Q3:React 和 Vue 的 Diff 算法有什么区别? #

特性 React Vue2 Vue3
策略 右侧遍历 双端比较 LIS
复杂度 O(n) O(n) O(n log n)
移动 可能多次移动 较优移动 最少移动
Key 要求 强烈建议 强烈建议 强烈建议

Q4:什么时候需要手动优化 Diff 性能? #

需要优化的情况:

  1. 大列表(1000+ 项):使用虚拟滚动
  2. 频繁更新的数据:使用 v-memo 或拆分组件
  3. 复杂的嵌套结构:合理使用 key、拆分组件
  4. 动画性能问题:使用 CSS transform、requestAnimationFrame

不需要优化的情况:

  1. 小型应用:Vue 的默认优化已经足够
  2. 数据量小(< 100 项):Diff 性能可以忽略
  3. 更新频率低:不必过度优化

Q5:如何调试 Diff 性能问题? #

// 1. 使用 Vue DevTools Performance 面板
// 2. 使用 console.time
console.time('render');
this.list = newList;
this.$nextTick(() => {
  console.timeEnd('render');
});
 
// 3. 使用 performance API
performance.mark('update-start');
this.list = newList;
this.$nextTick(() => {
  performance.mark('update-end');
  performance.measure('update', 'update-start', 'update-end');
  console.log(performance.getEntriesByName('update'));
});
 
// 4. Vue2 全局性能监控
Vue.config.performance = true;
 
// 5. Vue3 性能追踪
app.config.performance = true;

Q6:Vue3 的 Patch Flag 有多少种? #

const PatchFlags = {
  TEXT: 1,           // 动态文本内容
  CLASS: 2,          // 动态 class
  STYLE: 4,          // 动态 style
  PROPS: 8,          // 动态属性(非 class/style)
  FULL_PROPS: 16,    // 有动态 key 的属性
  HYDRATE_EVENTS: 32,// 有事件监听器
  STABLE_FRAGMENT: 64,         // 子元素顺序不会变的 fragment
  KEYED_FRAGMENT: 128,         // 带有 key 的 fragment
  UNKEYED_FRAGMENT: 256,       // 不带 key 的 fragment
  NEED_PATCH: 512,             // 需要进行非 props 比较
  DYNAMIC_SLOTS: 1024,         // 动态插槽
  HOISTED: -1,                 // 静态提升的节点
  BAIL: -2,                    // 退出 Diff 优化
};

Q7:如何理解 Vue3 的 Block? #

Block = 收集了动态子节点的特殊 VNode

// 普通 VNode
const vnode = {
  type: 'div',
  children: [...]
};
 
// Block
const block = {
  type: 'div',
  children: [...],
  dynamicChildren: [  // 只包含动态节点
    { type: 'span', children: '{{ title }}', patchFlag: 1 }
  ]
};
 
// Diff 时,只需要比较 dynamicChildren
// 而不是整个 children 树

九、总结速记 #

核心概念速记 #

概念 定义
虚拟 DOM 用 JS 对象描述真实 DOM 结构
Diff 算法 比较新旧虚拟 DOM,找出最小变化
Patch 将 Diff 结果应用到真实 DOM
Key VNode 的唯一标识,用于 Diff 判断相同节点

虚拟 DOM 优势 #

  1. 跨平台 - 可以渲染到任何目标
  2. 批量更新 - 合并多次修改,最小化 DOM 操作
  3. 声明式 - 关注"是什么",不关注"怎么做"

Diff 策略速记 #

Vue2:双端比较
- 头对头
- 尾对尾
- 头对尾
- 尾对头
- 查找匹配
 
Vue3:最长递增子序列(LIS)
- 前缀同步
- 后缀同步
- 建立 key 映射
- 计算 LIS
- 最少移动

Key 使用规则 #

✅ 用唯一 ID::key="item.id"
❌ 不用 index::key="index"
❌ 不用随机数::key="Math.random()"
❌ 不省略 key

性能优化速记 #

技术 作用
computed 缓存计算结果
v-once 只渲染一次
v-memo 条件缓存
拆分组件 减小 Diff 范围
虚拟列表 大列表优化
Patch Flag 靶向更新(Vue3)
Block Tree 收集动态节点(Vue3)

Vue2 vs Vue3 Diff 对比 #

特性 Vue2 Vue3
算法 双端比较 LIS
静态提升
Patch Flag
Block
事件缓存

最后更新:2026-03-29