Vue3源码剖析

本文对应的Vue3源码版本:3.5.13

工程架构设计

  • 【编译处理】将 vue 文件编译为函数或者对象
    • compiler-core
    • compiler-dom
    • compiler-sfc
    • compiler-ssr
  • 【响应式系统】将状态数据与视图更新链接起来
    • reactivity
  • 【运行时渲染】数据更新时需要重新渲染到页面中
    • runtime-core
    • runtime-dom
    • runtime-test // 用于测试
  • shared 文件夹下的代码是全局共享的,比如一些工具函数、常量等等
  • 【入口包】vue 文件夹下的代码是入口文件,提供开发者使用的内容

compiler-core、compiler-dom、compiler-sfc

compiler-core

compiler-core 是 Vue 3 的编译器核心,它负责将 Vue 模板编译为 JavaScript 渲染函数。

它是底层通用模块,不直接处理 .vue 文件,而是为各种 Vue 相关的编译场景提供基础能力(例如:在非 SFC 场景下编译纯模板字符串)。

  • parser 解析器(Parser 负责 “读懂” 模板结构)
    • 解析器的作用是将 Vue 模板字符串解析为 AST(抽象语法树)
    • 解析器的输出结果是一个 AST,AST 中包含了模板的结构信息
  • transformer 转换器(Transformer 负责 “理解” 模板语义)
    • 转换器的作用是对 AST 进行转换,将 AST 转换为一个更优化的 AST
    • 转换器的输出结果是一个优化后的 AST,优化后的 AST 中包含了更多的元数据信息
  • codegen 代码生成器(Codegen 负责 “翻译” 为可执行代码render 函数)
    • 代码生成器的作用是将 AST 转换为 JavaScript 代码
    • 代码生成器的输出结果是一个渲染函数,渲染函数的作用是将数据渲染到 DOM 中

整体流程

  1. 通过 vite、webpack 启动(打包)vue 项目,vite-plugin-vue(webpack vue-loader)
  2. 分词器(Tokenizer 负责 “切分” 模板为 tokens)
  3. 解析器(Parser 将分词 tokens 转换为 ast)
  4. 转换器(Transformer 负责优化 ast)
  5. 代码生成器(Codegen 将 ast 转换为 render 函数)
  6. 渲染函数(Render 负责 “执行” 渲染函数,将数据渲染到 DOM 中)
  7. 挂载(Mount 负责将渲染函数挂载到 DOM 中)

compiler-dom

compiler-dom

compiler-dom 是 Vue 3 的 DOM 编译器,它负责将 Vue 模板编译为 DOM 操作代码。

compiler-dom 是基于 compiler-core 的浏览器环境专用扩展,它在核心框架之上补充了浏览器 DOM 环境特有的编译逻辑,例如:

  • 处理 HTML 标签、属性(如 class、style)、自闭合标签等 DOM 特性。
  • 支持 v-html、v-text 等操作 DOM 的指令。
  • 生成与浏览器 DOM 操作匹配的渲染函数代码(如创建真实 DOM 节点的 API 调用)。

compiler-sfc

compiler-sfc
compiler-sfc 是 Vue 3 的单文件组件编译器,它负责将 Vue 单文件组件编译为 JavaScript 渲染函数。

是上层专用模块,基于 compiler-core 实现了对 SFC 格式的完整编译流程,是 Vue 单文件组件开发的核心依赖(如 Vite、Vue CLI
等工具都会用到它)。

响应式处理

reactivity文件夹

  • dep 依赖
    • track
    • trigger
    • notify
  • collection 依赖收集
    • MutableReactiveHandler,可读可写状态数据跟踪
    • 当数据被访问时,触发 proxy 的 get,track 依赖收集
    • 当数据被修改时,触发 proxy 的 set,trigger 依赖触发
  • ref、reactive
  • watch
  • effect 副作用

运行时处理

runtime-core

runtime-core 是 Vue 运行时的核心模块,与具体平台(如浏览器、Node.js、Weex 等)无关,包含了 Vue 最核心的逻辑,是跨平台能力的基础。

主要作用:

  • 虚拟 DOM 处理:实现虚拟 DOM 的创建、更新、diff 算法等核心逻辑,是 Vue 高效更新视图的基础。
  • 组件系统:定义组件的生命周期、状态管理(如 setup、reactive、ref 等)、事件处理、Props 传递等核心功能。
  • 响应式系统集成:与 @vue/reactivity 模块协作,实现数据变化驱动视图更新的响应式机制。
  • 渲染调度:负责协调组件渲染的优先级、异步更新队列等,优化渲染性能。
  • 平台无关的渲染逻辑:定义了渲染器(Renderer)的抽象接口,不直接操作 DOM,而是通过抽象接口与具体平台交互。

简单说,runtime-core 是 Vue 的 “灵魂”,包含了所有不依赖具体平台的核心逻辑,是实现跨平台的基础。

diff 算法

  • 简单 diff 算法
  • 双端 diff 算法
  • 快速 diff 算法(终极版本)

runtime-dom

runtime-dom 是 针对浏览器平台的运行时扩展,基于 runtime-core 实现,负责将核心逻辑与浏览器的 DOM 环境对接。

主要作用:

  • DOM 操作封装:提供浏览器特有的 DOM 操作方法,如创建元素(createElement)、插入元素(insert)、设置属性(setAttribute)、事件绑定(addEventListener)等。
  • 处理浏览器特有属性和事件:例如 class、style 等属性的特殊处理,以及浏览器事件的兼容处理(如事件冒泡、默认行为等)。
  • 集成浏览器 API:将 runtime-core 的抽象接口映射到实际的浏览器 API,例如将虚拟 DOM 转换为真实 DOM。
  • 提供浏览器环境的渲染器:基于 runtime-core 的渲染器接口,实现浏览器环境下的具体渲染逻辑。

简单说,runtime-dom 是 Vue 与浏览器 DOM 交互的 “桥梁”,让 runtime-core 的核心逻辑能在浏览器中生效。

runtime-core 负责组件的状态管理、虚拟 DOM 构建和 diff 计算。

runtime-dom 负责将虚拟 DOM 转换为真实 DOM,并处理浏览器特有的交互(如点击事件、样式更新等)。

手写 reactive

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56

let targetMap = new weakMap()

enum ReactiveFlags {
IS_REACTIVE = '__v_isReactive',
}

const mutableReactiveHandler = {
get(target, key, receiver) {
// 只要被代理了且访问了这个属性就返回 true
if (key === ReactiveFlags.IS_REACTIVE) {
return true
}

// 收集依赖,当取值的时候,让响应式对象的属性与 effect 关联起来
track(target, key)
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
let oldValue = target[key] // 旧值
let r = Reflect.set(target, key, value, receiver) // 返回的是布尔类型

// 判断新旧值是否一样
// 当设置值的时候,让对应的 effect 执行
if (oldValue !== value) {
trigger(target, key, value, oldValue)
}
return r
}
}

export function createReactiveObject(target) {
// 必须是对象
if (!isObject(target)) {
return target
}

if (target[ReactiveFlags.IS_REACTIVE]) {
return target
}

// 重复代理 直接返回代理对象
const existingProxy = targetMap.get(target)
if (existingProxy) {
return existingProxy
}
// 代理对象
const proxy = new Proxy(target, mutableReactiveHandler)
// 缓存代理对象
targetMap.set(target, proxy)
return proxy
}

export function reactive(target) {
return createReactiveObject(target)
}

疑问:为什么要用 Reflect 而不是直接用 target[key]?

为了解决 this 指向问题

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const obj = {
name: 'jayhwang',
age: 18,
sex: 'male',
get aliasName() {
return this.name + '花鹿'
}
}
const proxy = new Proxy(obj, {
get(target, key) {
console.log('get', key)
return target[key]
},
set(target, key, value) {
target[key] = value
return true
// return Reflect.set(target, key, value)
}
})

console.log(proxy.aliasName)

执行上面的代码,我们会发现 在读取aliasName属性时,会触发get aliasName() { return this.name + '花鹿' },这里面有this.name,可是我们在读取this.name的时候,没有触发下面的 proxy 的 get 的打印,这就说明这个 this
指向并不对,他没有被代理到。

我们再看下es6Reflectget方法

基本语法:Reflect.get(target, propertyKey[, receiver])

  • 参数说明:
    • target:要获取属性值的目标对象
    • propertyKey:要获取的属性名称(字符串或 Symbol)
    • receiver(可选):如果 target 对象中指定了 getter,receiver 会作为 getter 函数的 this 值
  • 返回值:返回获取到的属性值

手写 effect 副作用函数

effect 函数是一个核心概念,它负责创建 “副作用”(effect),并建立响应式数据与副作用之间的依赖关系。当响应式数据发生变化时,依赖它的副作用会自动重新执行,这是
Vue 3 响应式系统实现自动更新的基础机制。

基本概念

“副作用” 指的是一段会影响外部环境的代码(例如修改全局变量、更新 DOM 等)。在 Vue 中,组件的渲染函数就是一种典型的副作用(因为它会生成
DOM)。effect 函数的作用就是包裹这段副作用代码,并追踪其中使用的响应式数据,形成 “依赖收集”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
export let activeEffect = null // 当前的 effect

// 清除依赖
export function cleanupEffect(effect) {
const {deps} = effect
if (deps.length) {
for (let i = 0; i < deps.length; i++) {
deps[i].delete(effect)
}
deps.length = 0
}
}

export class ReactiveEffect {

public active = true // 是否是激活的
public deps = [] // 依赖的响应式数据
public parent = null // 父级 effect
constructor(public fn, private scheduler) {
this.fn = fn
}

run() {
if (!this.active) {
return this.fn() // 关闭状态下,激活副作用
}
// 其余情况下,意味着副作用是激活状态

// 处理 effect 嵌套 effect 情况
try {
// 将当前的 effect 变成全局的,,取值的时候可以拿到全局的 effect
this.parent = activeEffect
activeEffect = this
cleanupEffect(this) // 清除依赖
return this.fn()
// 执行副作用函数,副作用函数里面的响应式数据的取值操作会触发前面写的 reactive 的 get 方法
// 在那里可以我们取到全局的 activeEffect
} finally {
activeEffect = this.parent
this.parent = null
}
}

// 停止依赖收集
stop() {
this.active = false // 当前的 effect 是激活的,让他失活
cleanupEffect(this)
}
}

export function effect(fn, options = {}) {
const _effect = new ReactiveEffect(fn, options.scheduler)
_effect.run() // 默认执行一次

const runner = _effect.run.bind(_effect) // 绑定 this 指向 _effect
runner.effect = _effect
return runner // 用于启动依赖收集
}

/*
* 收集依赖关系,建立映射表,一个响应式的 key 可能对应一个或多个 effect 副作用,例如:
* effect(()=>{
* document.title = obj.name
* )
* effect(()=>{
* console.log(obj.name)
* })
* 当 obj.name 变化时,两个 effect 都需要重新执行
* */
const targetMap = new weakMap() // 映射表
/*
* targetMap的数据结构举例:
* {
* obj: {
* name: new Set([effect1, effect2]),
* age: new Set([effect1])
* }
* }
*
* */

// 依赖收集
export function track(target, key) {
// 取值操作不是发生在 effect 中,就不需要执行副作用函数,直接 return
if (!activeEffect) {
return
}

let depsMap = targetMap.get(target)
if (!depsMap) {
// weakMap中的 key 只能是对象
targetMap.set(target, depsMap = new Map())
}

let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, dep = new Set())
}

trackEffect(dep)
}

export function trackEffect(dep) {
// 依赖不存在,那就收集依赖
let shouldTrack = !dep.has(activeEffect)
if (shouldTrack) {
dep.add(activeEffect)
activeEffect.deps.push(dep)
}
}

// 依赖触发
export function trigger(target, key, newValue, oldValue) {
const depsMap = targetMap.get(target)
if (!depsMap) {
return
}
const dep = depsMap.get(key)
if (!dep) {
return
}
triggerEffect(dep)
}

export function triggerEffect(dep) {
/*
* 为什么要用 effects 重新接收一份?
* 答:防止依赖触发时,依赖发生变化,导致无限循环
* dep 是 set 类型的,在下方执行 run 方法的时候,会触发依赖清空操作
* 接着又会触发 this.fn() 方法,这就又触发了依赖收集
* 这样一边清空一边收集,操作的都是同一份 set,就会导致无限循环
* 所以要重新定义一个新变量来接收
* */
const effects = [...dep] // 新变量接收一份
effects.forEach(effect => {
if (effect.scheduler) {
effect.scheduler()
} else {
effect.run()
}
})
}

手写计算属性 computed

computed 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 默认不会执行,只有读取的时候才会执行
let fullName = computed(() => {
return obj.firstName + obj.lastName
})

// 多次读取 computed 属性,只有第一次会执行,后面的读取都从缓存中取
console.log(fullName.value)
console.log(fullName.value)
console.log(fullName.value)
// 当依赖的值变化了,会重新执行
obj.firstName = 'hello'
console.log(fullName.value)

// vue3中计算属性也具备依赖收集的功能
effect(() => {
console.log(fullName.value)
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
import {isFunction} from '@vue/shared'

const noop = () => {
}

class ComputedRefImpl {
dep = undefined // 依赖的响应式数据
__v_isRef = true // 表示需要 .value 来调用
_dirty = true; // 表示是否需要重新计算
_value = undefined; // 缓存值
effect = undefined; // 副作用函数

constructor(getter, setter) {
this.getter = getter
this.setter = setter
this.effect = new ReactiveEffect(this.getter, () => {
this._dirty = true
triggerEffect(this.dep)
})
}

// 属性访问器,取值的时候依赖收集
get value() {
// 如果有全局 effect,意味着这个计算属性在 effect 中执行的
if (activeEffect) {
trackEffect(this.dep || (this.dep = new Set()))
}

// 依赖收集
track(this, 'value')
if (this._dirty) {
this._value = this.effect.run()
this._dirty = false
}
return this._value
}

// 属性设置器
set value(newValue) {
this.setter(newValue)
}
}

export function computed(getterOrOptions) {
// 判断是否是函数
let onlyGetter = isFunction(getterOrOptions)

let getter
let setter

// 只有 getter 没有 setter,说明是只读属性
if (onlyGetter) {
getter = getterOrOptions
setter = noop
} else {
// 有 getter 有 setter,说明是可读写属性
getter = getterOrOptions.get
setter = getterOrOptions.set || noop
}

return new ComputedRefImpl(getter, setter)
}

手写 ref

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
export function ref() {
return new RefImpl()
}

function toReactive(value) {
return isObject(value) ? reactive(value) : value
}

class RefImpl {
dep = undefined
__v_isRef = true // ref标识
_value = undefined

constructor(value) {
this._value = toReactive(value)
}

get value() {
// 依赖收集
if (activeEffect) {
trackEffects(this.dep || (this.dep = new Set()))
}
return this._value
}

set value(newValue) {
if (newValue !== this._value) {
// 更新值
this._value = toReactive(newValue)
// 触发更新
triggerEfect(this.dep)
}
}
}

ref 和 reactive 的区别

ref

  • 用于创建基本类型数据的响应式数据,当然也可以包装引用类型(objectarray
  • 对于基本类型:创建一个{value:原始值}的包装对象,通过重写 valuegetter(收集依赖)和 setter(触发更新)实现响应式。
  • 对于引用类型:内部会自动调用 reactive 方法,将引用类型转换为响应式对象,因此 ref(obj).value 本质是一个 reactive 对象。
  • 在 js 中必须通过.value访问和修改值;在模板中可以直接访问,自动解包,不需要.value
  • 引用类型的 ref 如果直接替换整个对象,新对象仍会被转为响应式,不丢失响应式

reactive

  • 用于创建引用类型数据的响应式代理,无法直接处理基本类型数据(会警告且不生效)。
  • 通过Proxy代理整个对象,拦截对象的getset操作,实现响应式。如果对象存在嵌套属性,则递归处理。
  • 无论在模板还是脚本中,都可以直接访问和修改属性,无需.value
  • 不能直接替换整个对象,否则会丢失响应式。
  • 解构会丢失响应式,需要通过toReftoRefs处理,如:const {name,age} = toRefs(user)

watch 和 watchEffect 的区别

watch

  • 显式指定依赖,需要手动声明要监听的数据源,仅当依赖变化时才会执行回调。
  • 默认懒执行,初始化的时候不会自动执行回调,仅当被监听的数据变化时才触发。如果要初始化执行一次,需手动配置immediate: true
  • 能获得新旧值,回调函数接收两个参数:newVal(变化后的值)、oldVal(变化前的值),方便对比数据前后变化状态。
  • 延迟执行,默认情况下,watch 会在数据变化后立即执行回调。如果要延迟执行,需配置flush: 'post',在 Dom 更新后执行。

watchEffect

  • 自动追踪依赖,无需手动声明依赖,自动追踪回调函数内部使用的所有响应式数据,只要函数内部访问的像原始数据发生变化,就会触发回调。
  • 默认立即执行,初始化时会立即执行一次回调函数,无需配置immediate: true
  • 无参数,无法直接获得响应式数据变化前后的值。

如何将 template 转换为 render 函数

template 是声明式的模板语法,最终会被 vue 编译器编译为 render 函数,用于生成虚拟DOM。

将 template 转换为 render 函数,本质是将模板的标签、属性、指令、插值等语法,转换为通过 h 函数描述的 javascript 逻辑。

h 函数

Vue 提供的创建虚拟节点的函数,语法为:h(tag,props,children)

  • tag:节点的标签名或组件,如 'div''span' 等。
  • props:节点的属性/指令/事件,如 {id:'app',class:'container'} 等。
  • children:节点的子节点,如 'hello world'[h('div','hello world')] 等。

示例

1
2
3
4
5
6

<template>
<div class="container">
Hello, Vue!
</div>
</template>
1
2
3
4
5
import {h} from 'vue'

render() {
return h('div', {class: 'container'}, 'Hello, Vue!')
}

vue 的 diff 算法原理

Vue 的 diff 算法是虚拟 DOM 的核心组成部分,核心目的是通过对比新旧虚拟 DOM 数的差异,计算出最小化的 DOM 操作,从而提高渲染效率。

patch 方法

patch 方法是 Vue 中用于对比新旧虚拟 DOM 数差异并更新真实 DOM 的核心函数。

diff 算法的核心设计原则

同层比较,不跨层级

虚拟 DOM 是树形结构,diff 算法只对比同一层级的节点,不跨层级比较,算法复杂度为 O(n)。

先判断节点是否“可复用”

对比节点时,首先判断两个节点是否为“相同节点”,只有可复用的节点才需要进一步比较细节,否则直接销毁旧节点并创建新节点。

判断是否为“相同节点”的依据:

  • 标签名相同
  • key 相同
  • 是否为注释节点(isComment)
  • 对比组件节点,组件类型相同

Vue2的 diff 算法核心

当两个节点被判定为“相同节点”后,会进入详细对比流程,分为属性对比子节点对比

属性对比

对比新旧节点的属性(attrs、class、style 等),找出差异并更新到真实 DOM:

子节点对比

Vue2采用的双指针法(头尾指针),具体流程如下:

  1. 初始化指针:
    • 旧节点的头指针(oldStartIdx)
    • 旧节点的尾指针(oldEndIdx)
    • 新节点的头指针(newStartIdx)
    • 新节点的尾指针(newEndIdx)
  2. 四种快速对比场景
    • 头头对比
    • 尾尾对比
    • 旧头新尾对比
    • 旧尾新头对比
  3. key 映射表查找(处理乱序场景)
    • 为旧节点列表创建 key → 索引 的映射表(oldKeyToIdx),快速定位新节点的 key 在旧列表中是否存在。
    • 若找到对应节点:判断是否为相同节点(避免 key 重复导致误判),若是则复用并移动到对应位置,否则创建新节点。
    • 若未找到:直接创建新节点插入。
  4. 清理剩余节点
    当某一侧的指针遍历完成后:
    • 若旧节点指针未遍历完,将剩余节点全部删除
    • 若新节点指针未遍历完,将剩余节点全部创建并插入到对应位置

Vue3对 diff 算法的优化

  1. 静态标记:编译时对节点进行了标记,标记了该节点哪些是动态的(需要对比)。diff 算法在对比时,会跳过静态节点,只对比动态节点,从而提高对比效率。
  2. 最长递增子序列:处理列表乱序时,通过最长递增子序列算法算出最少的移动次数,减少 DOM 移动操作。
  3. 数结构打平:将虚拟 DOM 数的节点按层级打平为数组,通过索引快速访问,减少递归遍历的开销

vue 中 key 的作用

在 Vue 中,key 是一个特殊的属性,主要用于标识虚拟 DOM(VNode)节点的唯一性,其核心作用是帮助 Vue 的 diff
算法更高效、更准确地识别节点的变化,从而优化 DOM 更新性能并避免状态错乱

作为节点的 “唯一标识”,辅助 diff 算法识别可复用节点

Vue 的 diff 算法在对比新旧虚拟 DOM 时,首要任务是判断 “两个节点是否为同一个节点”(即是否可复用)。而 key
是判断的核心依据之一(配合标签名、组件类型等)。

  • 当两个节点的 key 相同(且其他条件匹配,如标签名一致)时,Vue 会认为它们是 “同一个节点”,会尝试复用旧节点并仅更新差异(如属性、文本内容)。
  • 当 key 不同时,Vue 会判定为 “不同节点”,会直接销毁旧节点并创建新节点,不会尝试复用。

这一机制大幅减少了 diff 算法的复杂度:通过 key 可以快速定位到可复用的节点,避免对 “看似不同但实际可复用” 的节点进行不必要的销毁和重建。

在列表渲染中避免 “节点复用错误”,保证状态正确

在使用 v-for 渲染列表时,key 是必须的(Vue 官方强制推荐),其核心作用是避免因节点复用导致的状态混乱。

错误使用:不使用 key 或使用索引(index)作为 key

优化 DOM 更新性能,减少不必要的操作

当列表发生排序、插入、删除等操作时,key 能帮助 diff 算法快速找到 “移动的节点”,从而仅通过 DOM 移动(而非销毁 +
重建)完成更新,大幅提升性能。

例如,对列表 [A, B, C] 排序为 [C, B, A]:

  • 有 key 时,diff 算法能通过 key 识别出节点只是位置变化,直接移动 DOM 元素即可。
  • 无 key 时,diff 算法可能无法识别移动,会销毁所有旧节点并重建新节点,性能损耗大。

总结

key 的核心作用是给节点一个 “唯一且稳定的身份标识”,帮助 Vue 的 diff 算法:

  • 准确识别可复用的节点,避免状态错乱;
  • 高效处理列表的增删改查,减少 DOM 操作,优化性能。

在实际开发中,列表渲染必须使用唯一 ID 作为 key,这是保证列表状态正确和性能优化的关键实践。


Vue3源码剖析
https://zouhualu.github.io/20241215/vue3源码剖析/
作者
花鹿
发布于
2024年12月15日
许可协议