字数
1830 字
阅读时间
8 分钟
一、Vue 2 响应式原理:Object.defineProperty
Vue 2 的响应式基于 Object.defineProperty 对对象的属性进行拦截,核心是通过“数据劫持”+“依赖收集”实现视图与数据的联动。
1. 核心机制
- 数据劫持:对数据对象的每个属性(包括嵌套属性)通过
Object.defineProperty定义getter和setter,拦截属性的“读取”和“修改”操作。 - 依赖收集:当属性被读取时(
getter触发),收集当前依赖(即使用该属性的组件/ Watcher);当属性被修改时(setter触发),通知所有依赖更新。
2. 实现步骤
- 初始化响应式:通过
Observer类将数据对象(data)转为响应式。遍历对象的所有属性,对每个属性调用Object.defineProperty定义getter/setter。javascriptclass Observer { constructor(value) { this.value = value; this.walk(value); // 遍历对象属性 } walk(obj) { Object.keys(obj).forEach(key => { defineReactive(obj, key, obj[key]); }); } } function defineReactive(obj, key, val) { // 递归处理嵌套对象 new Observer(val); // 依赖收集器(每个属性对应一个 Dep) const dep = new Dep(); Object.defineProperty(obj, key, { get() { // 收集依赖(当前活跃的 Watcher) if (Dep.target) { dep.depend(); } return val; }, set(newVal) { if (newVal === val) return; val = newVal; // 通知依赖更新 dep.notify(); } }); } - 依赖收集器(Dep):每个响应式属性对应一个
Dep实例,用于存储依赖该属性的Watcher。 - Watcher:组件渲染、
watch选项、computed等都会创建Watcher实例,当依赖的属性变化时,Watcher会触发更新(如重新渲染组件)。
视图中会⽤到 data 中某 key,这称为依赖。同⼀个 key 可能出现多次,每次都需要收集出来⽤⼀个 Watcher 来维护它们,此过程称为依赖收集。 多个 Watcher 需要⼀个 Dep 来管理,需要更新时由 Dep 统⼀通知。
3. 局限性
- 无法监听对象新增/删除属性:
Object.defineProperty只能拦截已存在的属性,新增属性(如this.obj.newKey = 1)或删除属性(如delete this.obj.key)不会触发更新,需通过Vue.set/this.$set或Vue.delete手动处理。 - 无法监听数组索引和长度变化:数组的
length属性修改(如this.arr.length = 0)或通过索引修改元素(如this.arr[0] = 1)不会触发更新,Vue 2 对数组的 7 个方法(push、pop等)进行了重写,以支持响应式。 - 深度监听性能损耗:初始化时需递归遍历所有属性,对于嵌套较深的大对象,初始化性能较差。
二、Vue 3 响应式原理:Proxy
Vue 3 改用 Proxy 实现响应式,解决了 Vue 2 的诸多局限性,同时保留了“依赖收集”的核心思想。
1. 核心机制
- 代理拦截:通过
Proxy创建数据对象的代理(Proxy实例),拦截对象的所有操作(包括属性读取、修改、新增、删除、数组方法调用等),而非针对单个属性。 - 懒代理:默认只对当前层级对象创建代理,嵌套对象的代理会在首次访问时动态创建(懒加载),优化初始化性能
WeakMap<target, Map<key, Set<effect>>> - 第一层:WeakMap 的键是被代理的原始对象(target),值是一个 Map(用于映射该对象的属性与依赖)。 - 第二层:Map 的键是对象的属性(key),值是一个 Set(用于存储依赖该属性的所有副作用函数)。 - 第三层:Set 中存储的是具体的副作用函数(effect),即当 target.key 变化时需要执行的函数。
2. 实现步骤
- 创建响应式对象:通过
reactive函数创建Proxy实例,定义handler拦截器对象,覆盖多种操作(get、set、deleteProperty、has、ownKeys等)。javascriptfunction reactive(target) { return new Proxy(target, { get(target, key, receiver) { const res = Reflect.get(target, key, receiver); // 收集依赖(与 Vue 2 的 Dep 类似,这里用 track 函数) track(target, key); // 若属性值是对象,递归创建代理(懒代理) if (isObject(res)) { return reactive(res); } return res; }, set(target, key, value, receiver) { const oldValue = Reflect.get(target, key, receiver); if (oldValue === value) return true; const result = Reflect.set(target, key, value, receiver); // 通知依赖更新(trigger 函数) trigger(target, key); return result; }, deleteProperty(target, key) { const hadKey = hasOwn(target, key); const result = Reflect.deleteProperty(target, key); if (hadKey) { // 删除属性时通知更新 trigger(target, key); } return result; } }); } - 依赖收集与触发:
track函数:在get拦截时调用,记录“目标对象 + 属性”与当前依赖(effect函数)的映射关系。trigger函数:在set/deleteProperty等拦截时调用,根据“目标对象 + 属性”找到所有依赖的effect函数并执行(触发更新)。
effect函数:替代 Vue 2 的Watcher,用于包裹需要响应式执行的逻辑(如组件渲染函数),当依赖的属性变化时,effect会重新执行。
3. 优势
- 支持所有对象操作:天然支持新增属性(
obj.newKey = 1)、删除属性(delete obj.key)、数组索引修改(arr[0] = 1)、length修改等,无需手动调用set/delete。 - 懒代理优化性能:仅在访问嵌套对象时才创建代理,避免 Vue 2 中递归遍历的性能损耗,尤其适合大对象。
- 更全面的拦截能力:
Proxy可以拦截in、for...in、Object.keys等操作,响应式覆盖范围更广。 - 支持 Map/Set 等集合类型:Vue 3 对
Map、Set、WeakMap、WeakSet等原生集合类型提供了响应式支持,通过拦截其get、set、add等方法实现。
三、核心差异总结
| 特性 | Vue 2(Object.defineProperty) | Vue 3(Proxy) |
|---|---|---|
| 拦截粒度 | 单个属性 | 整个对象(所有属性及操作) |
| 新增/删除属性 | 不支持(需手动 $set/$delete) | 原生支持 |
| 数组操作 | 仅重写 7 个方法,不支持索引/长度修改 | 支持所有数组操作(索引、长度、方法) |
| 嵌套对象处理 | 初始化时递归遍历 | 懒代理(访问时才创建代理) |
| 集合类型支持 | 不支持(需手动处理) | 支持 Map/Set 等原生集合 |
| 性能(大对象) | 初始化较慢(全量递归) | 初始化更快(懒加载) |
四、总结
Vue 2 的响应式基于 Object.defineProperty,通过拦截属性的 getter/setter 实现,但存在对新增属性、数组操作的局限性;Vue 3 改用 Proxy 代理整个对象,从根本上解决了这些问题,同时通过懒代理优化性能,支持更全面的响应式场景。这一变化是 Vue 3 性能提升和开发体验优化的重要基础。