25-11-03-Vue3的DOM操作与diff算法


学习《Vue.js 设计与实现》的笔记

目录

命令类型

声明式和命令式:
<input v-model="value"/>为例,这段代码是声明式的,是 Vue 提供的“语法糖”。书写这段声明式代码,本质上是在调用渲染函数这类命令式代码。

虚拟 DOM、模板字符串、Javascript 都能挂载、更新 DOM。

三种 DOM 操作方式的挂载与更新逻辑

  1. JavaScript(原生 DOM API)
  • 挂载:直接通过document.createElement创建节点,appendChild插入 DOM 树(如const div = document.createElement('div'); document.body.appendChild(div))。
  • 更新:手动定位需要修改的节点,调用 DOM API 更新(如div.textContent = 'new content')。

特点

  • 效率“理论上可能最高”——如果开发者能精准定位到最小更新范围,避免冗余操作,效率可以接近最优。但实际开发中,复杂场景下手动维护更新逻辑极易出错(如漏更、多更),反而可能写出低效代码。
  1. 虚拟 DOM(如 Vue 的 VNode)
  • 挂载
    1. 先创建虚拟 DOM 对象(描述节点类型、属性、子节点等,如{ type: 'div', props: { id: 'app' }, children: [] });
    2. 框架通过渲染函数(render)将虚拟 DOM 转换为真实 DOM(递归调用createElement),插入页面。
  • 更新
    1. 数据变化时生成新的虚拟 DOM;
    2. 通过 Diff 算法对比新旧虚拟 DOM,找出最小更新差异(如属性变化、子节点增删移);
    3. 只将差异部分转换为真实 DOM 操作(如patchProps更新属性、insertBefore移动节点)。

特点

  • “在复杂场景下效率更稳定”——Diff 算法会自动优化更新范围,避免开发者手动操作的疏漏。但相比“最优手动操作”,多了虚拟 DOM 创建和 Diff 的计算开销(这部分开销在 Vue3 中通过静态标记、编译时优化大幅降低)。
  1. 模板字符串(如innerHTML
  • 挂载:通过字符串拼接描述 DOM 结构(如const html =
    ${content}
    ; element.innerHTML = html),浏览器解析字符串并创建 DOM 树。
  • 更新:重新拼接完整的模板字符串,通过innerHTML覆盖原有内容(即使只有一个字符变化,也会销毁旧 DOM 树并重建)。

特点

  • 效率最低——每次更新都会销毁整个旧 DOM 树(包括解绑事件、移除子节点),重新解析字符串并创建新 DOM 树,性能开销极大(尤其包含大量节点或事件绑定时)。
  • 且存在 XSS 风险(若字符串包含未过滤的用户输入),Vue 等框架的模板会自动转义,本质是对模板字符串的安全封装,而非直接使用原生innerHTML

效率对比

在 DOM 操作的简单场景下 Javascript 最高效,虚拟 DOM 次之,模板字符串还得经过 parsing,效率最低。
但通过 Javascript 和模板字符串加载的心智负担重,而虚拟 DOM 不需要手动管理所有 DOM 操作细节,开发者只需要关注数据变化。因此,在复杂场景下使用虚拟 DOM 的效率最高。

VUE3 的 Diff

  1. 先对比父节点的 type 和 key,一致且父节点是动态节点(即会发生改变的节点,如传入数据可能变动的节点;与之相比的是静态节点)时,进一步对比节点的 props、children 等内容,进行子节点 Diff 环节;
  2. 对有多个子节点的情况,(双端 Diff 算法)先对比新旧 DOM 链表的首尾,通过 key 值判断节点是否相同:
    以对比头节点为例,如果新旧 DOM 链表的 key 值一样,则可以“直接”复用,否则尝试尾尾;之后再尝试头尾、尾头,后两种如果可以复用,则移动对应 DOM 节点;
  3. 确定可复用的首尾部分后,先遍历剩余的oldChildren(旧节点列表),构建keyToOldIdx的映射表(把后续遍历newChildren+oldChildren的 O(n^2)的查找操作优化成 O(n));
    再遍历newChildren,通过keyToOldIdx判断当前节点是否在旧列表中;
    • 若不存在,直接创建新节点;
    • 若存在,记录旧节点的索引,生成 “需要处理的索引数组”(source);
      最后source数组对进行“最长递增子序列”的计算,不移动该序列对应的节点,只移动其它节点,最大化复用原有的节点;
  4. 对剩余部分,根据 key 与实时处理标识 j,判断节点的新增、移动与删除。
    “j” 是最长递增子序列的 “指针”,用于标记当前已处理到的 “无需移动” 节点的位置。遍历 source 数组时:
    • 若当前节点索引不在最长递增子序列中,说明需要移动,从 j 对应的位置插入;
    • 遍历结束后,若旧列表还有剩余节点,直接删除;若新列表还有剩余节点,直接创建。

Vue3 Diff 的核心优化点(笔记缺失,需补充)

  1. 静态标记(PatchFlags):编译时给动态节点打上标记(如TEXTPROPS),Diff 时仅处理带标记的节点,跳过静态节点,减少对比范围;
  2. 最长递增子序列优化:Vue2 中未排序的子节点 Diff 会导致大量移动,Vue3 通过该算法将移动次数降到最低,时间复杂度从 O(n²)优化为 O(n log n);
  3. key 映射表:快速查找可复用节点,避免遍历查找的冗余计算。

文章作者: Qijia Huang
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Qijia Huang !
  目录