目录
- 前言
- [一、Vue 的编译系统](#一、Vue 的编译系统)
-
- 1、总揽
- [2、为什么 Vue 需要"编译"?](#2、为什么 Vue 需要“编译”?)
- 3、编译的三个阶段(核心流程)
-
- (1)、Parse:把模板变成"结构树"
- (2)、Transform:在树上做"优化标记"
- [(3)、Generate:生成 render 函数](#(3)、Generate:生成 render 函数)
- [4、Vue 编译做了哪些"聪明优化"?](#4、Vue 编译做了哪些“聪明优化”?)
-
- [(1)、静态提升(Hoist Static)](#(1)、静态提升(Hoist Static))
- [(2)、 PatchFlag(补丁标记)](#(2)、 PatchFlag(补丁标记))
- [(3)、Block Tree(动态节点分块)](#(3)、Block Tree(动态节点分块))
- 5、编译发生在什么时候?
- [6、编译系统在 Vue 里的位置](#6、编译系统在 Vue 里的位置)
- 7、总结
- [二、Vue 响应式系统](#二、Vue 响应式系统)
-
- 1、总览
- [2、Vue2 vs Vue3 响应式差异](#2、Vue2 vs Vue3 响应式差异)
- 3、核心结构总览(源码思维)
- 4、Reactive:响应式对象是怎么创建的
- 5、依赖收集是怎么发生的?(track)
- 6、触发更新是怎么发生的?(trigger)
- [7、effect 不只是渲染](#7、effect 不只是渲染)
- [8、ref 为什么存在?](#8、ref 为什么存在?)
- [9、computed 的缓存原理](#9、computed 的缓存原理)
- [10、watch vs watchEffect](#10、watch vs watchEffect)
- [11、调度器(为什么不会每改一次就更新 DOM)](#11、调度器(为什么不会每改一次就更新 DOM))
- 12、响应式系统解决了什么问题?
- [13、Vue 响应式系统脑图](#13、Vue 响应式系统脑图)
- [三、Vue 的虚拟 DOM 和 Diff](#三、Vue 的虚拟 DOM 和 Diff)
- [四、Vue 组件的更新过程](#四、Vue 组件的更新过程)
-
- 1、起点:响应式数据发生变化
- 2、调度阶段:进入更新队列(异步批处理)
- [3、真正开始组件更新:执行组件的 effect](#3、真正开始组件更新:执行组件的 effect)
- [4、render 阶段:生成新的虚拟 DOM](#4、render 阶段:生成新的虚拟 DOM)
- [5、patch 阶段:Diff + 更新 DOM](#5、patch 阶段:Diff + 更新 DOM)
-
- [(1)、节点类型不同 → 直接替换](#(1)、节点类型不同 → 直接替换)
- [(2)、类型相同 → 复用 DOM,只更新变化部分](#(2)、类型相同 → 复用 DOM,只更新变化部分)
- [(3)、子节点是数组 → 进入核心 Diff 算法](#(3)、子节点是数组 → 进入核心 Diff 算法)
- 6、更新完成后
前言
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 树会被保存为"旧树",用于下一次对比。
组件更新结束,页面已经是最新状态。