Proxy 与 Object.defineProperty
对比两种数据劫持方式及其在 Vue 响应式中的应用
问题
Proxy 与 Object.defineProperty 有什么区别?为什么 Vue3 选择 Proxy 替代 Object.defineProperty?
解答
Object.defineProperty
Vue2 使用的方案,通过定义属性的 getter/setter 实现劫持。
// 基本用法
const obj = {}
let value = 'hello'
Object.defineProperty(obj, 'msg', {
get() {
console.log('读取 msg')
return value
},
set(newVal) {
console.log('设置 msg:', newVal)
value = newVal
}
})
obj.msg // 读取 msg
obj.msg = 'hi' // 设置 msg: hi
// Vue2 风格的响应式实现
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
get() {
console.log(`get ${key}`)
return val
},
set(newVal) {
if (newVal === val) return
console.log(`set ${key}:`, newVal)
val = newVal
}
})
}
// 遍历对象所有属性
function observe(obj) {
if (typeof obj !== 'object' || obj === null) return
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
}
const data = { name: 'Tom', age: 18 }
observe(data)
data.name // get name
data.age = 20 // set age: 20
data.gender = 'M' // 新增属性,无法监听!
Proxy
Vue3 使用的方案,代理整个对象。
// 基本用法
const obj = { msg: 'hello' }
const proxy = new Proxy(obj, {
get(target, key, receiver) {
console.log('读取', key)
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
console.log('设置', key, ':', value)
return Reflect.set(target, key, value, receiver)
}
})
proxy.msg // 读取 msg
proxy.msg = 'hi' // 设置 msg : hi
// Vue3 风格的响应式实现
function reactive(obj) {
return new Proxy(obj, {
get(target, key, receiver) {
console.log(`get ${key}`)
const result = Reflect.get(target, key, receiver)
// 深层对象递归代理
if (typeof result === 'object' && result !== null) {
return reactive(result)
}
return result
},
set(target, key, value, receiver) {
console.log(`set ${key}:`, value)
return Reflect.set(target, key, value, receiver)
},
deleteProperty(target, key) {
console.log(`delete ${key}`)
return Reflect.deleteProperty(target, key)
}
})
}
const data = reactive({ name: 'Tom', age: 18 })
data.name // get name
data.age = 20 // set age: 20
data.gender = 'M' // set gender: M ✅ 新增属性也能监听
delete data.age // delete age ✅ 删除属性也能监听
数组处理对比
// Object.defineProperty 处理数组
const arr = [1, 2, 3]
arr.forEach((item, index) => {
Object.defineProperty(arr, index, {
get() {
console.log(`get [${index}]`)
return item
},
set(val) {
console.log(`set [${index}]:`, val)
item = val
}
})
})
arr[0] // get [0]
arr[0] = 9 // set [0]: 9
arr[3] = 4 // 新增索引,无法监听!
arr.push(5) // 无法监听!Vue2 需要重写数组方法
// Proxy 处理数组
const arr = reactive([1, 2, 3])
arr[0] // get 0
arr[0] = 9 // set 0: 9
arr[3] = 4 // set 3: 4 ✅ 新增索引能监听
arr.push(5) // get push, get length, set 4: 5, set length: 5 ✅ 原生方法能监听
对比表格
| 特性 | Object.defineProperty | Proxy |
|---|---|---|
| 监听新增属性 | ❌ 需要 Vue.set | ✅ 自动监听 |
| 监听删除属性 | ❌ 需要 Vue.delete | ✅ 自动监听 |
| 监听数组索引 | ❌ 需要重写方法 | ✅ 自动监听 |
| 监听粒度 | 单个属性 | 整个对象 |
| 性能 | 初始化时递归遍历 | 惰性代理,访问时才递归 |
| 兼容性 | IE9+ | 不支持 IE |
关键点
- Object.defineProperty 只能劫持已存在的属性,Proxy 代理整个对象
- Proxy 支持 13 种拦截操作(get、set、deleteProperty、has、ownKeys 等)
- Proxy 配合 Reflect 使用,保证正确的 this 指向
- Vue3 的 Proxy 是惰性代理,性能更好(访问时才递归,而非初始化时全量递归)
- Proxy 无法被 polyfill,这是 Vue3 不支持 IE 的原因
目录