一、Vue2 响应式原理
Vue2 的响应式系统基于 Object.defineProperty() 实现。当一个 Vue 实例创建时,Vue 会遍历 data 选项中的所有属性,使用 Object.defineProperty() 将这些属性转换为 getter/setter。这样,当这些属性的值发生变化时,Vue 能够检测到并自动更新与之绑定的 DOM 元素。
核心机制:
- Object.defineProperty():拦截属性的读取和设置操作。
- Dep(依赖):每个属性都有一个依赖收集器,用于跟踪哪些 DOM 元素依赖于该属性。
- Watcher(观察者):每个 DOM 绑定都对应一个 watcher 实例,当依赖的属性值变化时,watcher 会收到通知并更新 DOM。
具体实现
1. 对象初始化与属性遍历
- 目标:将普通对象转换为响应式对象。
- 实现:
- Vue2 在初始化数据时,递归遍历对象的所有属性(包括嵌套对象)。
- 示例对象:
1
2
3
4const data = {
name: "Vue",
info: { version: 2 }
}; - 遍历过程:
- 外层属性 name → 转换为响应式。
- 嵌套属性 info.version → 递归转换为响应式。
2. 使用 Object.defineProperty 劫持属性
- 核心逻辑:通过重写属性的 get 和 set 方法,拦截读写操作。
- 代码实现:
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// 简化的 Vue2 响应式实现
function defineReactive(obj, key, val) {
const dep = new Dep(); // 创建依赖收集器
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
// 收集依赖:将当前 watcher 添加到 dep 中
if (Dep.target) {
dep.depend();
}
return val;
},
set(newVal) {
if (val === newVal) return;
val = newVal;
// 触发更新:通知所有依赖此属性的 watcher
dep.notify();
}
});
}
// 依赖收集器
class Dep {
constructor() {
this.subs = []; // 存储所有依赖(watcher)
}
depend() {
if (Dep.target) {
this.subs.push(Dep.target);
}
}
notify() {
this.subs.forEach(watcher => watcher.update());
}
}
Dep.target = null;3. 依赖收集(Getter 阶段)
- 触发时机:当属性被访问(如模板渲染、计算属性计算时)。
- 依赖对象:Watcher(每个组件实例对应一个渲染 Watcher)。
- 过程:
- 执行 data.name 的 getter 方法。
- 当前活跃的 Watcher(如组件的渲染 Watcher)被记录到 Dep.target。
- 将 Watcher 添加到属性的依赖管理器 dep 中(通过 dep.addSub)。
4. 触发更新(Setter 阶段)
- 触发时机:当属性被修改(如 data.name = “New Vue”)。
- 过程:
- 执行 setter 方法,更新属性值。
- 调用 dep.notify(),通知所有关联的 Watcher 执行更新(如重新渲染组件)。
关键角色与关系
角色 | 作用 |
---|---|
Observer | 递归遍历对象,调用 defineReactive 将属性转换为响应式 |
Dep | 依赖管理器,存储所有 Watcher,负责通知更新(发布-订阅模式) |
Watcher | 具体依赖(如组件渲染函数),收到通知后执行更新操作 |
示例代码流程
1 | // 原始数据 |
Vue2 响应式的局限性
1. 无法检测新增/删除属性:
- 动态添加或删除对象属性时,需通过 Vue.set() 或 Vue.delete() 手动处理。
- 示例:Vue.set(data, “newProp”, 123)。
2. 数组监听不足:
- 通过重写数组方法(push, pop 等)实现响应式。
- 直接通过索引修改(arr[0] = 1)或修改 length 无效。
3. 深层响应式性能问题
- 初始化时需要递归遍历所有嵌套对象,对大型数据结构性能较差。
二、Vue3响应式原理
Vue3 的响应式系统基于 Proxy 和 Reflect 实现,完全重写了响应式核心。Proxy 可以直接监听整个对象的变化,而不需要像 Vue2 那样逐个属性转换。
核心机制:
- 它不是 劫持属性,而是 代理对象 本身。
- Proxy:拦截对象的所有操作(读取、设置、删除等),实现更全面的响应式监听。
- Reflect:提供与 Proxy 拦截方法对应的反射 API,确保原始对象的默认行为被正确处理。
- Vue3实现响应式需要自己根据需要声明,而Vue2需要放在data中,这也是Vue3函数式编程的体现之一。总的来说,可通过reactive()、ref()方法来声明一个响应式对象。
具体实现
1. 对象初始化与代理创建
- 目标:将普通对象转换为响应式对象。
- 实现:
- Vue3 使用 Proxy 对象替代 Vue2 的 Object.defineProperty,从而更优雅地实现响应式。
- 示例对象:
1
2
3
4const data = {
name: "Vue",
info: { version: 3 }
}; - 代理过程:
- 使用 reactive() 函数将 data 包装成响应式对象。
- 代理会拦截对 data 及其嵌套属性的访问和修改。
2. 使用 Proxy 拦截属性操作
相比 Vue2,它做了:
- get 读取数据时,执行 track 追踪数据。
- set 改变数据时,通过 trigger 触发数据相应逻辑执行。
- 使用 Reflect 更好的配合 proxy 去使用。
核心逻辑:通过 get 和 set 捕获器,拦截属性的读写操作。
代码实现:
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// 简化的 Vue3 响应式实现
function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
// 收集依赖
track(target, key);
// 使用 Reflect 获取原始值,支持继承
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
// 使用 Reflect 设置值,支持继承
const result = Reflect.set(target, key, value, receiver);
// 触发更新
trigger(target, key);
return result;
},
deleteProperty(target, key) {
// 使用 Reflect 删除属性
const result = Reflect.deleteProperty(target, key);
// 触发更新
trigger(target, key);
return result;
}
});
}
// 依赖收集函数
function track(target, key) {
if (!activeEffect) return;
// 创建 target 和 key 对应的依赖映射
let depsMap = targetMap.get(target);
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()));
}
let dep = depsMap.get(key);
if (!dep) {
depsMap.set(key, (dep = new Set()));
}
// 将当前活跃的 effect 添加到依赖中
dep.add(activeEffect);
activeEffect.deps.push(dep);
}
// 触发更新函数
function trigger(target, key) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
const effects = new Set();
const computedRunners = new Set();
// 获取 key 对应的依赖
if (key !== void 0) {
const dep = depsMap.get(key);
if (dep) {
dep.forEach(effect => {
if (effect.computed) {
computedRunners.add(effect);
} else {
effects.add(effect);
}
});
}
}
// 先执行 computed 类型的 effect,再执行普通 effect
computedRunners.forEach(run => run());
effects.forEach(run => run());
}3. 依赖收集(Getter 阶段)
触发时机:当属性被访问(如模板渲染、计算属性计算时)。
依赖对象:effect(Vue3 中用于追踪响应式依赖的函数)。
过程:
- 访问 data.name 时,get 捕获器被触发。
- 调用 track() 函数,将当前的 effect 与该属性关联。
- 依赖关系被存储在全局的 targetMap 中,便于后续更新时通知。
4. 触发更新(Setter 阶段)
- 触发时机:当属性被修改(如 data.name = “New Vue”)。
- 过程:
- 修改 data.name 时,set 捕获器被触发。
- 调用 trigger() 函数,找到所有与该属性关联的 effect 并执行。
- 关联的 effect 会重新运行,导致视图更新或其他副作用。
关键角色与关系
角色 | 作用 |
---|---|
Proxy | 使用 get 和 set 捕获器拦截属性访问和修改,替代 Vue2 的 Object.defineProperty |
track | 在 get 阶段记录依赖关系,将 effect 与访问的属性关联起来 |
trigger | 在 set 阶段触发更新,找到所有关联的 effect 并执行 |
effect | 响应式副作用函数,当被追踪的属性发生变化时重新执行 |
示例代码流程
1 | // 模拟 Vue3 的实现 |
三、其他相关特性
1. Vue2 中的 $set 和 $delete
由于 Object.defineProperty 的限制,Vue2 提供了 $set 和 $delete 方法用于动态地添加或删除对象的属性,并确保这些属性能够被监听。
2. Vue3 中的 ref 和 reactive
ref 用于创建包含单一值的响应式对象,reactive 用于创建包含多个属性的响应式对象,实现响应式数据的深度监听。
3. Vue3 在处理深度监听时更加高效和准确
可以通过设置 reactive 函数的第二个参数为 true 来实现深度监听。
4. Vue3 为什么还要配合使用 Reflect?
来看下面这段 Proxy 的代码实现:
1 | const proxy = new Proxy(target, { |
你可能会问:为啥不直接写 target[key] 呢?
1 | get(target, key) { |
还真的不行!
Reflect 是 ES6 新增的内置对象,一般与 Proxy 组合使用。使用 Reflect.get 有几个好处:
- 保证 this 指向正确(特别是在 class 中访问 getter)
- 更准确地返回属性值,避免默认行为丢失
- 和 Proxy 内部行为保持一致
来看个示例对比:
1 | class A { |
你会发现,如果不使用 Reflect.get,那么 this 会指向原始对象,而不是代理对象,导致 getter、setter 行为不一致。所以说,Proxy 是 Vue3 响应式的关键核心,但是配合 Reflect 使用才会更安全稳定。
5. Vue3 响应式 注意点
- 无法 Polyfill
Vue3 使用了 Proxy,这是 ES6 才引入的高级特性,而它 无法被降级或 Polyfill。
意味着如果你想支持 IE11 或更老的浏览器 —— Vue3 本身就无法运行。这也是 Vue 团队决定 Vue3 完全放弃 IE 支持的一个重要原因。 - 响应式过度追踪问题
在使用 watchEffect 时,Vue 会自动收集你访问的所有响应式数据作为依赖。
这听起来方便好用,但如果你在一个 effect 里访问了太多变量,每一个变量变化都会触发整个副作用函数重新执行。假如这些变量变化频繁,或者你在其中做了复杂逻辑操作,性能就可能拉胯。1
2
3watchEffect(() => {
console.log(obj.a, obj.b, obj.c) // 都是依赖
})
解决办法是更精准使用 watch,或者拆分多个 watchEffect,控制依赖粒度。 - 响应式丢失陷阱:结构赋值后就不是响应式了!因为响应式代理的是对象本身,结构赋值相当于把值 复制 出来了,脱离了响应式系统。
1
2
3
4
5
6const state = reactive({ count: 0 })
const { count } = state // 断开响应式
console.log(count) // 这只是普通数值,变化不会响应式更新
正确做法:
- 用 toRefs() 保留响应式:
1
const { count } = toRefs(state)
- 或者干脆用 storeToRefs()(如果你在 Pinia 中)
- 或者不要结构赋值,直接用 state.count
四、Vue2 VS Vue3
特性 | Vue2 | Vue3 |
---|---|---|
原理 | Object.defineProperty() | Proxy + Reflect |
监听方式 | 需预先定义所有属性,新增属性需使用 Vue.set,递归遍历属性逐个劫持 | 代理整个对象,自动监听属性添加和删除 |
新增/删除属性支持 | 不支持(需特殊 API) | 原生支持 |
数组监听 | 直接修改索引无效,需重写方法(如 push、splice) | 直接监听索引和 length 变化 |
数据结构支持 | 仅普通对象 | Map、Set、WeakMap 等 |
嵌套监听 | 递归遍历所有层级 | 懒监听,性能更优 |
性能 | 初始化时递归遍历,性能较低 | 惰性监听,性能更优 |
TypeScript | 对 TypeScript 支持有限,类型推导不完整 | 完全支持 TypeScript |
五、Vue3到底强在哪里
Vue3 的响应式系统,远不止于 把 defineProperty 换成了 Proxy 这么简单。
真正让它 更强 的地方是:
- 全面的响应式监听:Proxy 可以监听新增、删除属性,解决了 Vue2 无法监听对象属性新增/删除的问题,对对象、数组、Map、Set 统统支持;
- 代码逻辑层级清晰可控:track/trigger/effect 模块解耦;
- 嵌套对象处理:采用懒代理(Lazy Proxy)机制,只在访问嵌套对象时才创建代理,减少初始开销。
- 清晰的依赖管理:通过 WeakMap 和 Set 高效存储依赖关系,避免了 Vue2 中复杂的依赖追踪逻辑。
- 性能优化:仅在属性访问和修改时执行必要的操作,减少了不必要的性能开销。
- 支持更多编程范式:支持组合式、TS、服务端渲染等场景;
- 源码阅读体验感更好:语义清晰,调试方便,扩展性强。