⚡ 30 秒速记

  • 核心判断:虚拟节点是对界面结构的描述,渲染器通过挂载与更新把声明式描述映射为宿主环境操作
  • 原理主线:围绕 「减小DOM操作的性能开销」、「尽可能的复用 DOM 元素」、「另一个思路 - 双端比较」 建立输入、状态变化与输出之间的因果关系
  • 文章范围:解析Vue渲染器的核心Diff算法,讲解其在高效更新DOM、优化性能、处理多子节点时的原理与实现方式,帮助开发者理解和提升前端渲染效率。
  • 边界与代价:key、节点类型和稳定序列决定复用边界;虚拟 DOM 解决可预测更新,不保证永远比手写 DOM 快
  • 工程落地:性能分析要看实际提交次数、节点移动和组件边界,不能只用“减少 DOM 操作”解释

核心 Diff 处理的是新旧两组子节点,目标是尽可能复用和移动 DOM,而不是全部删除后重建。 没有 key 时,只能按位置比较公共长度,多出的新节点执行挂载,多出的旧节点执行移除;列表换序时,这可能把本可复用的节点内容逐个改写。加入稳定且唯一的 key 后,渲染器才能建立新旧节点的映射,先调用 patch 保证内容正确,再根据旧索引是否保持递增判断哪些节点需要移动。key 解决的是身份识别,不代表节点一定无需更新。

这篇文章不要按 API 清单来背。先用上面的 Mind Map 建立全局结构,再通过交互 DEMO 观察正常路径和边界路径如何改变状态;阅读正文时重点核对每一步的输入、负责执行的参与者、产生的中间状态以及最终可观察结果。遇到版本敏感结论,要把“历史实现”“当前行为”和“工程兼容策略”分开说明;遇到性能或架构取舍,则用实际指标、失败现象和验证手段支撑判断。

版本校准: 本文出现 Object.definePropertyDepWatcher 和双端 Diff 时,主要描述 Vue 2 实现;Vue 3 使用 Proxyeffect 与新的渲染器路径。Vue 2 已于 2023-12-31 结束维护,新项目应以 Vue 3 为基线,旧项目参考 Vue 2 EOL 官方说明 制定迁移与安全策略。

# 减小DOM操作的性能开销

上一章我们讨论了渲染器是如何更新各种类型的 VNode 的,实际上,上一章所讲解的内容归属于完整的 Diff 算法之内,但并不包含核心的 Diff 算法。那什么才是核心的 Diff 算法呢?看下图:

我们曾在上一章中讲解子节点更新的时候见到过这张图,当时我们提到只有当新旧子节点的类型都是多个子节点时,核心 Diff 算法才派得上用场,并且当时我们采用了一种仅能实现目标但并不完美的算法:遍历旧的子节点,将其全部移除;再遍历新的子节点,将其全部添加,如下高亮代码所示:

function patchChildren(
  prevChildFlags,
  nextChildFlags,
  prevChildren,
  nextChildren,
  container
) {
  switch (prevChildFlags) {
    // 省略...

    // 旧的 children 中有多个子节点
    default:
      switch (nextChildFlags) {
        case ChildrenFlags.SINGLE_VNODE:
          // 省略...
        case ChildrenFlags.NO_CHILDREN:
          // 省略...
        default:
          // 新的 children 中有多个子节点
          // 遍历旧的子节点,将其全部移除
          for (let i = 0; i < prevChildren.length; i++) {
            container.removeChild(prevChildren[i].el)
          }
          // 遍历新的子节点,将其全部添加
          for (let i = 0; i < nextChildren.length; i++) {
            mount(nextChildren[i], container)
          }
          break
      }
      break
  }
}
webapp
公众号
开发者导航
切换夜间模式
点击侧边栏上一篇
点击侧边栏下一篇
折叠侧边栏
收起全部