Vue 组件的更新过程(编译系统 + 响应式系统 + 虚拟 DOM & Diff)

目录

前言

Vue 的"编译系统 + 响应式系统 + 虚拟 DOM 和 Diff",此三者前后衔接、各司其职的一条生产线。

typescript 复制代码
编译系统  →  决定"怎么生成 VNode"
响应式系统 →  决定"什么时候重新生成 VNode"
虚拟 DOM & Diff → 决定"VNode 变了怎么更新真实 DOM"

你可以把 Vue 想成一个自动化工厂:

模块 在工厂里的角色 现实职责
编译系统 设计图纸生成器 把 template 变成 render 函数
响应式系统 传感器系统 监测数据变化
虚拟 DOM + Diff 施工机器人 精准更新真实 DOM

核心四骤:

  • 第一步:编译系统 ------ "设计阶段"
  • 第二步:响应式系统 ------ "监听阶段"
  • 第三步:虚拟 DOM ------ "生成新模型"
  • 第四步:Diff 算法 ------ "找差异"

把三者串成完整流程:

typescript 复制代码
// 编译系统(设计阶段)
① 你写 template
      ↓
② 编译系统 → 变成 render 函数

-----------------------------------------
// 响应式系统(监听阶段)
③ render 执行时读取数据
      ↓
④ 响应式系统记录依赖关系

-----------------------------------------
// 虚拟 DOM & Diff(生成新模型)
⑤ 数据变化
      ↓
⑥ 响应式系统通知 render 重新执行
      ↓
⑦ 生成新的虚拟 DOM
      ↓
// 虚拟 DOM & Diff(找差异)
⑧ Diff 对比新旧虚拟 DOM
      ↓
⑨ 最小化更新真实 DOM

各自解决的问题:

模块 解决的问题 关键能力
编译系统 模板怎么高效变成代码 静态提升、PatchFlag
响应式系统 数据变了谁需要更新 依赖收集、触发更新
虚拟 DOM & Diff 页面怎么高效更新 同层对比、最小 DOM 操作

一、Vue 的编译系统

Vue 真正的性能优势,很大一部分来自编译系统。

1、总揽

一句话理解 Vue 编译系统

  • 把你写的"像 HTML 的模板",变成"高性能的 JS 渲染代码"。

也就是:

typescript 复制代码
template  →  编译  →  render 函数  →  虚拟 DOM

你写得很优雅,Vue 在背后给你翻译成高效率代码。

2、为什么 Vue 需要"编译"?

你平时写的是:

typescript 复制代码
<template>
  <div>
    <p>{{ count }}</p>
    <button @click="count++">+1</button>
  </div>
</template>

但浏览器不认识这些"Vue 语法"。浏览器只认识:

typescript 复制代码
document.createElement(...)

所以 Vue 必须先做一件事:

  • 把模板翻译成 JS 函数

👉 这个过程就叫 ------ 编译(compile)

3、编译的三个阶段(核心流程)

Vue 编译不是一步到位,而是 3 步流水线:

typescript 复制代码
模板字符串
   ↓ ① 解析 parse
AST 抽象语法树
   ↓ ② 转换 transform
优化后的 AST
   ↓ ③ 生成 generate
render 渲染函数

我们一个个拆开。

(1)、Parse:把模板变成"结构树"

Vue 先把模板拆成一棵"树":

typescript 复制代码
<div>
  <p>{{ count }}</p>
</div>

会变成类似:

typescript 复制代码
{
  type: 'Element',
  tag: 'div',
  children: [
    {
      type: 'Element',
      tag: 'p',
      children: [
        { type: 'Interpolation', content: 'count' }
      ]
    }
  ]
}

这棵树叫:

  • AST(抽象语法树)

👉 作用:让 Vue "看懂" 你的模板结构

(2)、Transform:在树上做"优化标记"

这一阶段是 Vue3 性能飞跃的关键。

Vue 会分析哪些地方是:

类型 举例 处理方式
静态内容 <h1>标题</h1> 永远不变,直接缓存
动态文本 {``{ count }} 标记为"需要更新"
事件 @click 生成事件监听代码

比如:

typescript 复制代码
<div>
  <h1>Hello</h1>
  <p>{{ count }}</p>
</div>

Vue 会发现:

  • <h1>Hello</h1> ------ 永远不变 ✅
  • <p>{{ count }}</p> ------ 依赖数据 ❗

于是打上"补丁标记"(patchFlag)

👉 目的:

  • 以后更新时,Vue 不用整个对比,只看"有变化标记的地方"

(3)、Generate:生成 render 函数

最后一步,把优化后的 AST 变成 JS 代码。

模板:

typescript 复制代码
<p>{{ count }}</p>

会变成类似:

typescript 复制代码
return (_openBlock(), _createElementBlock("p", null, toDisplayString(count), 1))

这个函数就是:

  • render 函数(渲染函数)

它执行后会生成 虚拟 DOM。

4、Vue 编译做了哪些"聪明优化"?

这才是重点:

  • 静态提升
  • PatchFlag(补丁标记)
  • Block Tree(动态节点分块)

(1)、静态提升(Hoist Static)

typescript 复制代码
<div>
  <h1>标题</h1>
  <p>{{ count }}</p>
</div>
  • <h1> 永远不变 → 提前创建一次 → 后面复用

避免重复创建 DOM 节点。

(2)、 PatchFlag(补丁标记)

Vue 会给动态节点打标签:

标记 含义
TEXT 只有文本会变
CLASS class 会变
PROPS 某些属性会变

更新时直接精准更新,不用全量 diff。

(3)、Block Tree(动态节点分块)

Vue3 把模板拆成:

  • 动态区块 和 静态区块

更新时只检查动态区块,大量减少对比成本。

5、编译发生在什么时候?

分两种情况:

场景 编译时机
.vue 单文件组件 构建阶段(Vite 打包时)
直接写 template 选项 浏览器运行时编译

推荐方式是 SFC,因为:

  • 构建期编译 → 运行时更快

6、编译系统在 Vue 里的位置

现在你可以把 Vue 运行流程串起来了:

typescript 复制代码
你写 template
      ↓
Vue 编译系统 → render 函数
      ↓
响应式数据变化
      ↓
render 执行 → 生成新的虚拟 DOM
      ↓
diff 算法对比 → 更新真实 DOM

👉 编译系统决定了:

  • render 函数"长什么样",
  • 从而决定后续更新"快不快"。

7、总结

Vue 编译系统就像一个:

  • 把"人类友好的模板"翻译成"机器高效执行代码"的翻译官

而且这个翻译官还会:

  • 提前找出不变的内容
  • 标记可能变化的部分
  • 让更新时只动"该动的地方"

所以 Vue 快,不只是因为虚拟 DOM,更因为它在 编译阶段就帮你做了大量优化。

二、Vue 响应式系统

1、总览

Vue 响应式 = 依赖收集系统 + 变更通知系统

  • 当数据被"读"时 → 记录谁依赖它
  • 当数据被"改"时 → 通知这些依赖重新执行

这套机制的核心就三个角色:

typescript 复制代码
数据        → 被代理
依赖(副作用) → 被收集
调度器      → 决定何时执行更新

2、Vue2 vs Vue3 响应式差异

版本 实现方式 缺点
Vue2 Object.defineProperty 不能监听新增/删除属性、数组索引
Vue3 Proxy 真正完整监听对象操作

Vue3 用的是:

typescript 复制代码
new Proxy(target, handler)

所以 Vue3 响应式本质是 对 对象 操作的拦截系统

3、核心结构总览(源码思维)

Vue 内部维护了一个超级重要的结构:

typescript 复制代码
WeakMap
  └── target (被代理对象)
        └── Map
              └── key
                    └── Set (effects)

翻译成人话:

  • 哪个对象的哪个属性,被哪些副作用函数依赖着

4、Reactive:响应式对象是怎么创建的

typescript 复制代码
const state = reactive({ count: 0 })

内部核心逻辑(伪代码):

typescript 复制代码
function reactive(target) {
  return new Proxy(target, {
    get(target, key) {
      track(target, key)   // 依赖收集
      return target[key]
    },
    set(target, key, value) {
      target[key] = value
      trigger(target, key) // 派发更新
      return true
    }
  })
}

关键就是两个函数:

  • track() ------ 收集依赖
  • trigger() ------ 触发更新

5、依赖收集是怎么发生的?(track)

依赖收集只会发生在:

  • 响应式数据被"副作用函数"读取时

那什么是副作用函数?

typescript 复制代码
effect(() => {
  console.log(state.count)
})

当 effect 执行时,Vue 会:

  • 记录当前 activeEffect
  • 读取 state.count
  • 触发 Proxy 的 get
  • 在 track() 里把 activeEffect 存起来

伪代码示例:

typescript 复制代码
let activeEffect

function effect(fn) {
  activeEffect = fn
  fn()
  activeEffect = null
}

function track(target, key) {
  if (!activeEffect) return
  depsMap[target][key].add(activeEffect)
}

6、触发更新是怎么发生的?(trigger)

当数据被修改:

typescript 复制代码
state.count++

触发 set → trigger:

typescript 复制代码
function trigger(target, key) {
  const effects = depsMap[target][key]
  effects.forEach(effect => effect())
}

于是刚才收集的副作用函数会重新执行,页面更新。

7、effect 不只是渲染

很多人以为 effect = 组件渲染,其实包括:

类型 本质
组件 render 一个 effect
computed 懒执行的 effect
watchEffect 带调度的 effect
watch 手动指定依赖的 effect

所以:

🔥 Vue 整个更新系统,都是 effect 在驱动

8、ref 为什么存在?

typescript 复制代码
const count = ref(0)

ref 本质是:

typescript 复制代码
{
  value: 0
}

只是对 .value 做了 reactive 代理。

为什么不用 reactive 处理基本类型?

因为:

typescript 复制代码
reactive(1) // 无法 Proxy

所以 ref 是 对基本类型的响应式包装器------将基本类型包装成可 reactive 的对象。

9、computed 的缓存原理

typescript 复制代码
const total = computed(() => state.count * 2)

computed 内部:

  • 本质是一个 lazy effect
  • dirty 标记

流程:

  • 第一次读取 → 执行 getter → 收集依赖
  • 依赖没变 → 直接返回缓存
  • 依赖变了 → dirty = true → 下次再算

👉 所以 computed = 带缓存的响应式副作用

10、watch vs watchEffect

watchEffect watch
自动收集依赖 手动指定依赖
立即执行 默认懒执行
更像 effect 更像监听器

底层都基于 effect,只是调度方式不同。

11、调度器(为什么不会每改一次就更新 DOM)

trigger 时不会直接执行 effect,而是交给调度器:

typescript 复制代码
effect(fn, {
  scheduler(job) {
    queueJob(job)
  }
})

Vue 会把多个更新合并,在 微任务 里批量执行。

👉 这就是 nextTick异步更新的来源。

12、响应式系统解决了什么问题?

在 Vue 之前:

  • 手动操作 DOM → 状态和 UI 容易不一致

Vue 做到:

typescript 复制代码
状态变化 → 自动通知依赖 → 自动更新 UI

这是一套:

🔥 基于依赖追踪的自动化更新系统

13、Vue 响应式系统脑图

typescript 复制代码
reactive / ref
      ↓
Proxy 拦截 get/set
      ↓
track() 收集依赖
trigger() 触发依赖
      ↓
effect 重新执行
      ↓
组件 render 重新生成 vnode
      ↓
diff → 更新 DOM

三、Vue 的虚拟 DOM 和 Diff

  • 虚拟 DOM = 轻量级 JS 对象表示 DOM
  • Diff 算法 = 高效找出最小更新量

简单理解:

  • Vue 不直接操作浏览器 DOM
  • 而是先在内存里模拟 DOM(虚拟 DOM)
  • 然后对比新旧虚拟 DOM,只更新必要的部分

1、为什么要虚拟 DOM?

浏览器 DOM 操作成本高:

  • 插入 / 删除节点 → 浏览器重排(reflow)
  • 样式改变 → 重绘(repaint)

如果每次状态变动都直接操作 DOM,性能会很差。

虚拟 DOM 的策略:

  • 在 JS 内存里做变化(快)
  • 计算差异(Diff)
  • 只更新真正需要变的 DOM(少)

2、虚拟 DOM 是什么?

虚拟 DOM 本质是:

typescript 复制代码
{
  type: 'div',
  props: { class: 'box' },
  children: [
    { type: 'p', children: ['count: 1'] }
  ]
}
  • type → 标签名或组件
  • props → 属性 / 事件
  • children → 子节点(文本 / vnode / 组件)

每个节点叫一个 VNode(虚拟节点)

Vue 运行时操作的是 vnode,而不是直接 DOM

3、虚拟 DOM 的更新流程

(1)、初次渲染

  • render 函数生成 vnode
  • Vue 调用 patch() 把 vnode 转成真实 DOM
  • 插入页面

(2)、数据变化:

  • reactive / ref 触发 effect
  • effect 重新执行 render → 生成新 vnode
  • Vue 比较新旧 vnode → 生成最小更新 → patch 到 DOM
typescript 复制代码
数据变化
   ↓
render() → 新 VNode
   ↓
旧 VNode + 新 VNode → Diff
   ↓
最小 DOM 操作

4、Diff 算法原理(核心优化)

Diff 的目标:

  • "最少操作,把旧 vnode 改成新 vnode"

Vue3 Diff 策略:

  • 同层比较
    • 不跨层 diff,只在同级子节点里做比较
    • 不移动 DOM 节点到另一层(避免复杂)
  • 头尾双指针优化
    • 同时从头和尾比对
    • 如果匹配就跳过
    • 剩余未匹配节点 → 精确计算差异
  • Key 优化
    • 给列表加 :key → Vue 用 key 对应新旧节点
    • 重新排列列表时,能最少移动 DOM
    • 没 key → 用索引对比 → 可能造成不必要重渲染
  • 静态提升 & PatchFlag
    • 静态节点不参与 diff
    • PatchFlag 标记动态节点
    • 只对动态节点做更新 → 极大减少对比

5、VNode Patch 的本质操作

typescript 复制代码
新 VNode          旧 VNode
   │                  │
   │                  └─ 不存在 → 创建 DOM
   │
   └─ 对比 type
         │
         ├─ 相同 → 更新 props / children
         └─ 不同 → 替换整个节点
  • type 不同 → replace
  • type 相同 → patch props + patch children
  • children → 递归 diff

6、列表 Diff(最复杂的场景)

typescript 复制代码
<li v-for="item in list" :key="item.id">{{ item.text }}</li>

Vue3 做法:

  • 双指针从头/尾比
  • key 对应查找 → 最小 DOM 移动
  • 新增节点 → create
  • 删除节点 → remove
  • 改动 → patch props / text

核心思想:尽量复用 DOM 节点

7、虚拟 DOM 优化背后的思路

  • 减少操作 DOM → 性能瓶颈最关键
  • 静态节点缓存 → render 时跳过
  • PatchFlag + key → 列表重排只动必要节点
  • 同层 diff → 减少全树递归复杂度

8、通俗比喻

  • 虚拟 DOM = 纸上模型
  • Diff = 比对新旧模型差异
  • DOM patch = 工人只搬动需要移动的东西

你改数据 → 纸上模型更新 → 找到差 → 工人只搬差 → 页面更新

9、总结

  • Vue 不直接操作 DOM → 避免性能损耗
  • 虚拟 DOM = JS 对象描述 DOM
  • Diff 算法 = 找最小更新量
  • PatchFlag & key & 静态提升 → 大幅优化性能
  • 这套机制让 Vue 在复杂页面也能快速更新

四、Vue 组件的更新过程

组件更新

= 响应式触发 → 调度更新 → 重新 render → patch 对比 → 更新真实 DOM

= effect → render → patch

Vue 组件的更新从响应式数据变化开始,通过 trigger 找到组件的渲染 effect,并将其加入调度队列异步执行。执行时会重新调用 render 生成新的虚拟 DOM 树,然后通过 patch 对比新旧 VNode,利用 Diff 算法以最小 DOM 操作更新页面,最后缓存新的 VNode 作为下一次更新的旧树。

"effect → render → patch" 的完整 源码流程:

typescript 复制代码
响应式数据变化
      ↓
trigger() 通知组件的 effect
      ↓
组件的 effect 重新执行
      ↓
render() 生成新的 VNode 树
      ↓
patch(oldVNode, newVNode)
      ↓
Diff 算法最小化更新真 DOM
      ↓
页面完成更新

1、起点:响应式数据发生变化

比如:

typescript 复制代码
const count = ref(0)

count.value++   // 👈 触发更新的源头

ref/reactive 内部会调用:

typescript 复制代码
trigger(target, key)

它会找到 所有依赖了这个数据的 effect

而组件在挂载时,已经把自己的更新逻辑注册成了一个 effect:

typescript 复制代码
instance.update = effect(componentUpdateFn)

所以这一步的结果是:

  • 这个组件被标记为"需要重新更新"

但注意:不会立刻更新 DOM。

2、调度阶段:进入更新队列(异步批处理)

Vue 不会数据一变就马上 render,而是做了优化:

typescript 复制代码
queueJob(instance.update)

也就是把组件的更新任务丢进一个 更新队列(job queue)

然后通过微任务统一执行:

typescript 复制代码
Promise.resolve().then(flushJobs)

好处是:

typescript 复制代码
count.value++
count.value++
count.value++

只会触发 一次组件更新 ------这就是 Vue 的 异步批量更新机制

3、真正开始组件更新:执行组件的 effect

队列开始刷新后,会执行:

typescript 复制代码
instance.update()

也就是当初注册的 effect:

typescript 复制代码
const componentUpdateFn = () => {
  if (!instance.isMounted) {
    // 首次挂载
  } else {
    // 👇 更新逻辑
    const nextTree = renderComponentRoot(instance)
    patch(prevTree, nextTree, container)
    instance.subTree = nextTree
  }
}

从这里开始,就进入 render → patch 阶段。

4、render 阶段:生成新的虚拟 DOM

typescript 复制代码
const nextTree = renderComponentRoot(instance)

干了什么?

执行编译后的 render 函数:

typescript 复制代码
function render(_ctx) {
  return openBlock(), createElementBlock("div", null, _ctx.count)
}

生成新的 VNode:

typescript 复制代码
{
  type: 'div',
  children: '2'
}

此时 Vue 手里有两棵树:

名称 含义
prevTree 上一次渲染生成的 VNode
nextTree 本次 render 新生成的 VNode

接下来进入核心对比。

5、patch 阶段:Diff + 更新 DOM

typescript 复制代码
patch(prevTree, nextTree, container)

这一步就是 虚拟 DOM Diff

(1)、节点类型不同 → 直接替换

typescript 复制代码
<div> → <span>

卸载旧 DOM,创建新 DOM

(2)、类型相同 → 复用 DOM,只更新变化部分

比如:

typescript 复制代码
<div class="a">1</div>
<div class="b">2</div>

Vue 会:

  • 复用原来的 div
  • 更新 class
  • 更新文本

(3)、子节点是数组 → 进入核心 Diff 算法

typescript 复制代码
<li v-for="item in list" :key="item.id" />

Vue 会:

  • 通过 key 找到可复用节点
  • 移动而不是重建
  • 用 最长递增子序列算法 减少 DOM 移动次数

最终做到:

  • 只改真正变了的 DOM,而不是整棵重建

6、更新完成后

typescript 复制代码
instance.subTree = nextTree

新的虚拟 DOM 树会被保存为"旧树",用于下一次对比。

组件更新结束,页面已经是最新状态。

相关推荐
我是伪码农3 小时前
Vue 智慧商城项目
前端·javascript·vue.js
小书包酱3 小时前
在 VS Code中,vue2-vuex 使用终于有体验感增强的插件了。
vue.js·vuex
Zhencode4 小时前
Vue3 响应式依赖收集与更新之effect
前端·vue.js
天下代码客4 小时前
使用electronc框架调用dll动态链接库流程和避坑
前端·javascript·vue.js·electron·node.js
weixin79893765432...5 小时前
Vue 渲染体系“三件套”(template 模板语法、h 函数和 JSX 语法)
vue.js·h函数·template 模板·jsx 语法
xkxnq5 小时前
第五阶段:Vue3核心深度深挖(第74天)(Vue3计算属性进阶)
前端·javascript·vue.js
Hilaku6 小时前
不要在简历上写精通 Vue3?来自面试官的真实劝退
前端·javascript·vue.js
竟未曾年少轻狂7 小时前
Vue3 生命周期钩子
前端·javascript·vue.js·前端框架·生命周期
TT哇7 小时前
【实习】数字营销系统 银行经理端(interact_bank)前端 Vue 移动端页面的 UI 重构与优化
java·前端·vue.js·ui