技术知识文章集合TECHNICAL ARCHIVE · 457 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTVUE

既然 Vue 可以通过响应式探测数据变化,为什么还需要 Virtual DOM Diff?

所属馆藏
Vue
文件格式
Markdown
原始路径
Vue/82-既然 Vue 通过数据劫持可以精准探测数据在具体dom上的变化,为什么还需要虚拟 DOM diff
本文目录9 个章节
  1. 一、先修正一个前提:Vue 不一定知道“具体 DOM 节点”变了
  2. 二、如果已经知道数据变了,为什么不能直接改 DOM
  3. 三、Virtual DOM Diff 解决的是什么问题
  4. 四、响应式和 Diff 的配合关系
  5. 五、一个状态影响多个节点的例子
  6. 六、Diff 并不意味着每次都全量比较
  7. 七、什么时候可以不用 Virtual DOM
  8. 八、面试中的推荐回答
  9. 参考资料

既然 Vue 可以通过响应式探测数据变化,为什么还需要 Virtual DOM Diff?

原文只有一个面试题链接。这里补充完整回答:响应式系统负责知道“哪些状态依赖发生变化”,Virtual DOM/renderer 负责知道“声明式 UI 应该如何变成新的宿主节点”。两者解决的是不同问题,不能相互替代。

一、先修正一个前提:Vue 不一定知道“具体 DOM 节点”变了

Vue 的响应式系统可以比较精确地知道:

哪个对象的哪个属性被读取
哪个属性后来被写入
哪些组件渲染 effect / watcher 依赖这个属性

但这不等于它天然知道某个属性对应 DOM 中的哪一个具体节点。例如:

<template>
  <section :data-count="count">
    <h1>{{ count }}</h1>
    <p v-if="count > 0">当前有数据</p>
  </section>
</template>

count 改变时,它影响 attribute、文本内容和条件分支。响应式系统主要知道当前组件的 render effect 读取了 count,并触发这个 effect 重新运行;“哪些 DOM 节点需要新增、修改、移动或删除”仍然属于渲染器和 patch 的工作。

更准确的模型是:

响应式系统:状态 → 依赖 / effect
渲染函数:依赖 → 新的 VNode 树
renderer:旧 VNode + 新 VNode → DOM 操作

Vue 2 的渲染 watcher 通常以组件渲染为粒度;Vue 3 使用 ReactiveEffect 和编译器提示进一步缩小工作范围,但仍不能把状态属性直接等同为单个 DOM 节点。

二、如果已经知道数据变了,为什么不能直接改 DOM

对于极简单的页面,当然可以手写:

import { ref, watch } from 'vue'

const count = ref(0)
const el = document.querySelector('#count')

watch(count, value => {
  el.textContent = String(value)
})

但真实页面很快会遇到问题:

  • 一个状态被多个文本、class、style、attribute 和组件同时使用;
  • v-if 会新增或删除整棵子树;
  • v-for 会插入、删除、排序和移动节点;
  • 子组件、slot、Transition、Teleport 有自己的渲染边界;
  • 表单 property 和 HTML attribute 的更新规则不同;
  • 异步组件、KeepAlive、SSR hydration 需要统一生命周期;
  • 多次状态修改需要批量调度,避免重复读写 DOM;
  • 业务代码如果直接保存 DOM 引用,状态和视图会强耦合。

因此,响应式系统只给出“需要重新计算某个渲染结果”的信号,不能替代一个通用的 UI reconciliation 层。

三、Virtual DOM Diff 解决的是什么问题

Virtual DOM 是对期望 UI 的可组合描述:

const oldVNode = {
  type: 'button',
  props: { class: 'normal' },
  children: '保存',
}

const newVNode = {
  type: 'button',
  props: { class: 'primary' },
  children: '保存中',
}

Diff/patch 可以把这两个描述转换为:

更新 button.class
更新 button.textContent
保留原有 button 节点、焦点和事件

如果节点类型和 key 不同,renderer 可能需要卸载旧节点并创建新节点;如果 children 是 keyed list,则需要匹配、移动、创建和删除。这个过程把 DOM 操作集中在框架内部,而不是散落在业务 watcher 中。

function patch(oldVNode, newVNode) {
  if (!sameVNode(oldVNode, newVNode)) {
    replace(oldVNode, newVNode)
    return
  }

  patchProps(oldVNode.el, oldVNode.props, newVNode.props)
  patchChildren(oldVNode.el, oldVNode.children, newVNode.children)
}

这段代码只是概念示意。真正的 Vue renderer 还要处理文本、注释、Fragment、组件、指令、Transition、Teleport、Suspense、SVG、SSR hydration 等类型。

四、响应式和 Diff 的配合关系

Vue 2.7

Object.defineProperty
       ↓
Dep 收集 render Watcher
       ↓
setter / 数组方法调用
       ↓
queueWatcher + nextTick
       ↓
vm._render() 生成新 VNode
       ↓
vm._update() / patch

Vue 2 的 getter/setter 能精确定位属性依赖,但渲染 watcher 通常重新执行组件 render,再由 patch 比较新旧 VNode。

Vue 3.5

Proxy / ref getter
       ↓
ReactiveEffect track
       ↓
trigger + scheduler
       ↓
组件 render effect 重新运行
       ↓
VNode patch

Vue 3 compiler 还会提供:

  • 静态节点提升;
  • Patch Flags;
  • Block Tree;
  • keyed children 的专门路径;
  • 动态后代扁平化。

因此 Vue 3 并不是“只要使用 Proxy 就不需要 Virtual DOM”,而是响应式、编译器和 renderer 一起工作,避免遍历和更新不需要变化的部分。

五、一个状态影响多个节点的例子

<script setup>
import { ref } from 'vue'

const count = ref(0)
</script>

<template>
  <button :disabled="count >= 3">
    点击次数:{{ count }}
  </button>
  <p v-if="count >= 3">已经达到上限</p>
</template>

count2 变成 3 时,至少有这些变化:

  1. button 的 disabled property 从 false 变成 true;
  2. button 文本从“点击次数:2”变成“点击次数:3”;
  3. 新增提示 p 节点;
  4. 事件、组件生命周期和后续状态仍需保持正确。

响应式系统负责触发这个组件的更新;VNode/renderer 负责根据前后 UI 描述提交这些变化。把三处 DOM 更新全部写在一个 watcher 中当然可以,但那实际上是在手写一个局部 renderer。

六、Diff 并不意味着每次都全量比较

“使用 Virtual DOM 就一定要完整遍历整棵树”也不准确。现代 Vue 会结合:

  • 组件更新边界;
  • 静态提升;
  • Patch Flags;
  • Block Tree;
  • key;
  • scheduler 批处理。

但也不能反过来说 Diff 一定产生理论上的最小 DOM 操作。不同类型节点、复杂动态 key、未优化 render function、组件边界和宿主平台都会影响实际工作量。

七、什么时候可以不用 Virtual DOM

Virtual DOM 是一种取舍,不是所有界面都必须使用:

  • 静态 HTML 可以直接由服务器输出;
  • Canvas/WebGL 通常有自己的场景树和绘制模型;
  • 专门的细粒度响应式系统可以直接订阅 DOM 绑定;
  • 小型交互用原生 DOM 可能更简单;
  • Vue 自定义 renderer 可以把同样的 VNode 协调逻辑适配到其他平台。

选择框架时应看更新模式、团队维护成本、SSR/可访问性要求和性能测量,而不是只比较“有没有 Virtual DOM”。

八、面试中的推荐回答

可以这样总结:

Vue 的响应式系统和 Virtual DOM Diff 解决的是两个层面的问题。响应式系统负责追踪状态被哪些渲染 effect 读取,并在状态变化后调度相关组件更新;render function 会生成新的 VNode。Virtual DOM renderer 再比较新旧 VNode,决定哪些属性、文本、子节点需要更新、插入、移动或删除,并统一处理组件、指令、slot 和不同宿主环境。Vue 2 依赖 defineProperty + Dep/Watcher,Vue 3 主要使用 Proxy + ReactiveEffect,但两者都仍需要 renderer。Vue 3 还通过静态提升、Patch Flags 和 Block Tree 减少 Diff 成本。响应式知道“状态依赖变了”,不等于直接知道“哪个 DOM 节点应该怎样改”。

参考资料

原文只有问题标题和讨论链接,本文补充了响应式、渲染 effect、VNode、Diff、编译器优化和直接 DOM 更新之间的关系。

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

支持搜索文章标题、所属分类和原始文档路径。

按分类浏览

10 COLLECTIONS