Object.defineProperty 与 Proxy 对比
前言
响应式系统是 Vue 框架的核心机制之一,通俗易懂的来说 vue2需要手动登记,只有被用到的才会被记录 ,vue3全自动监控。
一、Vue2 的响应式原理
- 核心实现:Object.defineProperty
Vue2 的响应式通过 数据劫持 实现,其核心是对对象属性的 getter 和 setter 进行拦截。
javascript
// 定义响应式对象
function defineReactive(obj, key, val) {
const dep = new Dep(); // 依赖收集容器(每个属性对应一个 dep)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
dep.depend(); // 收集依赖:将 Watcher 添加到 dep 中
return val;
},
set(newVal) {
if (newVal === val) return;
val = newVal;
dep.notify(); // 触发更新:通知所有 Watcher 执行回调
}
});
}
// 递归遍历对象属性
function observe(obj) {
if (typeof obj !== 'object' || obj === null) return;
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key]); // 登记监控 key
});
}
- 依赖收集与触发机制
- Dep 类:每个属性对应一个 Dep 实例,用于存储所有依赖该属性的 Watcher。
- Watcher 类:代表一个视图或计算属性的依赖,当数据变化时触发回调。
依赖收集流程
- 组件渲染时触发 getter。
- 将当前 Watcher(如渲染函数)添加到 Dep 的订阅列表中。
触发更新流程
- 属性被修改时触发 setter。
- 通过 dep.notify() 通知所有订阅的 Watcher 执行更新。
- 局限性
- 无法检测新增/删除对象属性
需使用 Vue.set() 或 Vue.delete() 强制触发响应。
javascript
// 动态属性添加 API
function set(target, key, val) {
if (Array.isArray(target)) {
target.length = Math.max(target.length, key)
target.splice(key, 1, val)
return val
}
if (key in target) {
target[key] = val
return val
}
const ob = target.__ob__
if (!ob) {
target[key] = val
return val
}
defineReactive(ob.value, key, val)
ob.dep.notify()
return val
}
- 数组需要特殊处理
Vue2 重写了数组的 push、pop 等方法,需通过原型链劫持实现响应式。
javascript
// 数组原型劫持
const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'
]
methodsToPatch.forEach(method => {
const original = arrayProto[method]
def(arrayMethods, method, function mutator(...args) {
const result = original.apply(this, args)
const ob = this.__ob__
// 处理新增元素
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
ob.dep.notify() // 手动触发更新
return result
})
})
- 性能开销大
初始化时递归遍历对象所有属性,对深层嵌套对象不友好。
如果对象有 1000 个属性,需要逐个递归,耗时较长。
二、Vue3 的响应式原理
1.核心实现:Proxy
简化版代码实现
javascript
// 响应式入口
function reactive(target) {
const handler = {
get(target, key, receiver) {
track(target, key); // 依赖收集
const res = Reflect.get(target, key, receiver);
if (typeof res === 'object' && res !== null) {
return reactive(res); // 递归代理嵌套对象(惰性代理)
}
return res;
},
set(target, key, value, receiver) {
const oldValue = target[key];
const result = Reflect.set(target, key, value, receiver);
if (oldValue !== value) {
trigger(target, key); // 触发更新
}
return result;
},
deleteProperty(target, key) {
const hadKey = Object.prototype.hasOwnProperty.call(target, key);
const result = Reflect.deleteProperty(target, key);
if (hadKey) {
trigger(target, key); // 触发更新
}
return result;
}
};
return new Proxy(target, handler);
}
// 依赖收集与触发(简化版)
const targetMap = new WeakMap(); // 存储所有响应式对象及其依赖
function track(target, key) {
if (!activeEffect) return;
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()));
}
dep.add(activeEffect); // 存储当前激活的 effect
}
function trigger(target, key) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
const effects = depsMap.get(key);
effects && effects.forEach(effect => effect());
}
- ref 的实现
javascript
class RefImpl {
constructor(value) {
this._value = isObject(value) ? reactive(value) : value
this.dep = new Set()
}
get value() {
trackRefValue(this) // 依赖收集
return this._value
}
set value(newVal) {
if (hasChanged(newVal, this._value)) {
this._value = isObject(newVal) ? reactive(newVal) : newVal
triggerRefValue(this) // 触发更新
}
}
}
function trackRefValue(ref) {
if (activeEffect) {
trackEffects(ref.dep)
}
}
function triggerRefValue(ref) {
triggerEffects(ref.dep)
}
- 核心改进
- 动态属性监听
支持对象属性的动态增删,无需特殊 API。 - 原生数组响应式
可直接通过索引修改数组或修改 length。
javascript
const arr = reactive([1, 2, 3]);
arr[0] = 10; // 触发更新
arr.length = 1; // 触发更新
- 惰性代理
只有被用到的属性才会被追踪。,减少初始化开销。 - 代码更简单
仅在实际使用的属性上触发更新,不需要处理各种特殊情况。
三、性能优化
- 大型对象初始化
javascript
// 包含 1000 个属性的对象
const bigData = { /* 1000 个属性 */ }
// Vue2:立即递归转换所有属性(耗时)
const vm = new Vue({ data: { bigData } })
// Vue3:按需代理(初始化极快)
const state = reactive(bigData)
- 动态属性操作
javascript
// Vue2 必须使用特殊API
Vue.set(vm.data, 'newProp', value)
// Vue3 直接操作
state.newProp = value
- 数组性能测试
javascript
// 10万条数据数组
const bigArray = new Array(1000).fill(null).map((_, i) => ({ id: i }))
// Vue2 需要 200ms+ 初始化
new Vue({ data: { bigArray } })
// Vue3 需要 <10ms 初始化
const reactiveArray = reactive(bigArray)
总结
- vue2手动登记,只有被用到的才会被记录,vue3全自动监控。
- vue3性能更优:惰性代理减少初始化开销。
- vue3代码更简洁。