`Object.defineProperty`和`Proxy`是Vue.js 2和Vue.js 3中用于实现数据响应式的两种不同的机制。当需要对对象进行拦截和监视时,JavaScript提供了两种主要的机制:`Object.defineProperty`和`Proxy`。
一、Object.defineProperty
Vue.js 2中使用的是`Object.defineProperty`来实现数据响应式。它通过在对象上定义访问器属性来拦截对属性的访问和修改操作,并在这些操作发生时触发相关的更新操作。具体来说,它会在对象上定义一个getter和setter函数,使得当属性被访问或修改时,能够触发依赖追踪和更新。这样,当数据发生变化时,Vue能够检测到变化并及时更新相关的视图。
(1)`Object.defineProperty`是一个用于修改现有对象属性特性或定义新属性的方法。
(2)通过`Object.defineProperty`可以定义属性的可写性(writable)、可枚举性(enumerable)和可配置性(configurable)。
(3)可以使用`get`和`set`方法定义属性的读取和写入行为,从而实现对属性的拦截和监视。
get:属性的 getter 函数,当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入 this 对象(由于继承关系,这里的 this 并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值
set:属性的 setter 函数,当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象。默认为 undefined
(4)例子:
定义一个响应式函数defineReactive
function update() {
app.innerText = obj.foo
}
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
get() {
console.log(`get ${key}:${val}`)
return val
},
set(newVal) {
if (newVal !== val) {
val = newVal
update()
}
},
})
}
调用defineReactive
,数据发生变化触发update
方法,实现数据响应式
const obj = {}
defineReactive(obj, 'foo', '')
setTimeout(() => {
obj.foo = new Date().toLocaleTimeString()
}, 1000)
在对象存在多个key
情况下,需要进行遍历
function observe(obj) {
if (typeof obj !== 'object' || obj == null) {
return
}
Object.keys(obj).forEach((key) => {
defineReactive(obj, key, obj[key])
})
}
如果存在嵌套对象的情况,还需要在defineReactive
中进行递归
function defineReactive(obj, key, val) {
observe(val)
Object.defineProperty(obj, key, {
get() {
console.log(`get ${key}:${val}`)
return val
},
set(newVal) {
if (newVal !== val) {
val = newVal
update()
}
},
})
}
当给key
赋值为对象的时候,还需要在set
属性中进行递归
set(newVal) {
if (newVal !== val) {
observe(newVal) // 新值是对象的情况
notifyUpdate()
}
}
上述例子能够实现对一个对象的基本响应式,但仍然存在诸多问题
现在对一个对象进行删除与添加属性操作,无法劫持到
const obj = {
foo: 'foo',
bar: 'bar',
}
observe(obj)
delete obj.foo // no ok
obj.jar = 'xxx' // no ok
当我们对一个数组进行监听的时候,并不那么好使了
const arrData = [1, 2, 3, 4, 5]
arrData.forEach((val, index) => {
defineProperty(arrData, index, val)
})
arrData.push() // no ok
arrData.pop() // no ok
arrDate[0] = 99 // ok
`Object.defineProperty`适用于对现有对象进行属性级别的拦截和监视,但它只能对已有的属性进行定义,无法对整个对象进行拦截。
可以看到数据的api
无法劫持到,从而无法实现数据响应式,
所以在Vue2
中,增加了set
、delete
API,并且对数组api
方法进行一个重写
还有一个问题则是,如果存在深层的嵌套对象关系,需要深层的进行监听,造成了性能的极大问题
总结:
- 检测不到对象属性的添加和删除
- 数组
API
方法无法监听到 - 需要对每个属性进行遍历监听,如果嵌套对象,需要深层监听,造成性能问题
二、Proxy
Vue.js 3中引入了`Proxy`作为实现数据响应式的新机制。`Proxy`是ES6中提供的一个强大的对象拦截器,它可以对目标对象进行拦截,并在拦截操作发生时执行自定义的操作。在Vue.js 3中,使用`Proxy`来代理对象,并在访问或修改属性时拦截操作并触发更新。相比`Object.defineProperty`,`Proxy`提供了更广泛和灵活的拦截能力。
(1)`Proxy`是ES6引入的一种强大而灵活的对象拦截和监视机制。
(2)通过创建`Proxy`对象,可以在目标对象上拦截并定制各种操作,如属性的读取(get)、写入(set)、删除(delete)、函数调用(apply)等。
(3)`Proxy`提供了一系列的`handler`方法,用于定义拦截操作的处理逻辑。
(4)例子:
定义一个响应式方法reactive
function reactive(obj) {
if (typeof obj !== 'object' && obj != null) {
return obj
}
// Proxy相当于在对象外层加拦截
const observed = new Proxy(obj, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver)
console.log(`获取${key}:${res}`)
return res
},
set(target, key, value, receiver) {
const res = Reflect.set(target, key, value, receiver)
console.log(`设置${key}:${value}`)
return res
},
deleteProperty(target, key) {
const res = Reflect.deleteProperty(target, key)
console.log(`删除${key}:${res}`)
return res
},
})
return observed
}
测试一下简单数据的操作,发现都能劫持
const state = reactive({
foo: 'foo',
})
// 1.获取
state.foo // ok
// 2.设置已存在属性
state.foo = 'fooooooo' // ok
// 3.设置不存在属性
state.dong = 'dong' // ok
// 4.删除属性
delete state.dong // ok
再测试嵌套对象情况,这时候发现就不那么 OK 了
const state = reactive({
bar: { a: 1 },
})
// 设置嵌套对象属性
state.bar.a = 10 // no ok
如果要解决,需要在get
之上再进行一层代理
function reactive(obj) {
if (typeof obj !== 'object' && obj != null) {
return obj
}
// Proxy相当于在对象外层加拦截
const observed = new Proxy(obj, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver)
console.log(`获取${key}:${res}`)
return isObject(res) ? reactive(res) : res
},
return observed
}
总结:
Object.defineProperty
只能遍历对象属性进行劫持
function observe(obj) {
if (typeof obj !== 'object' || obj == null) {
return
}
Object.keys(obj).forEach((key) => {
defineReactive(obj, key, obj[key])
})
}
Proxy
直接可以劫持整个对象,并返回一个新对象,我们可以只操作新的对象达到响应式目的
function reactive(obj) {
if (typeof obj !== 'object' && obj != null) {
return obj
}
// Proxy相当于在对象外层加拦截
const observed = new Proxy(obj, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver)
console.log(`获取${key}:${res}`)
return res
},
set(target, key, value, receiver) {
const res = Reflect.set(target, key, value, receiver)
console.log(`设置${key}:${value}`)
return res
},
deleteProperty(target, key) {
const res = Reflect.deleteProperty(target, key)
console.log(`删除${key}:${res}`)
return res
},
})
return observed
}
Proxy
可以直接监听数组的变化(push
、shift
、splice
)
const obj = [1, 2, 3]
const proxtObj = reactive(obj)
obj.psuh(4) // ok
Proxy
有多达 13 种拦截方法,不限于apply
、ownKeys
、deleteProperty
、has
等等,这是Object.defineProperty
不具备的
正因为defineProperty
自身的缺陷,导致Vue2
在实现响应式过程需要实现其他的方法辅助(如重写数组方法、增加额外set
、delete
方法)
// 数组重写
const originalProto = Array.prototype
const arrayProto = Object.create(originalProto)
['push', 'pop', 'shift', 'unshift', 'splice', 'reverse', 'sort'].forEach(method => {
arrayProto[method] = function () {
originalProto[method].apply(this.arguments)
dep.notice()
}
});
// set、delete
Vue.set(obj,'bar','newbar')
Vue.delete(obj),'bar')
`Proxy`提供了更广泛的拦截和监视能力,可以对目标对象的各种操作进行拦截和定制处理逻辑。
可以根据需要使用不同的`handler`方法,如`get`、`set`、`deleteProperty`、`apply`等,来实现特定操作的拦截和监视。
总结:
`Object.defineProperty`是一种用于修改对象属性特性或定义新属性的方法,适用于属性级别的拦截和监视。而`Proxy`是一种更强大和灵活的对象拦截和监视机制,可以对目标对象的各种操作进行拦截和定制处理逻辑。根据需求和场景,可以选择合适的机制来实现对象的拦截和监视。
三、Vue3.0 里为什么要用 Proxy API 替代 defineProperty API
最核心的原因是性能,展开来说如下。
-
因为 Proxy 代理的直接是整个对象,例如对象中有 100 个属性,用 defineProperty 劫持至少需要循环 100 次,而 proxy 至少一次搞定。
-
defineProperty 对数组中存在大量元素的劫持操作性能不好,所以 Vue2 并没有直接使用 defineProperty 对数组进行劫持,而是提供了额外的方法来处理数组的响应式,例如 $set,其实换成 proxy 就不存在这个问题了,当然 $set 方法也就没有必要存在了。
-
Vue2 中,劫持数据的操作在实例创建完成就已经进行完毕了,所以对对象后续新增的属性是劫持不到的,也就意味着后续新增的属性是不具备响应式能力的,所以 Vue 不得不提供了 $set 方法。而换成 proxy 就不存在这个问题了,因为它劫持的整个对象,后续添加的属性也是属于这个对象的,那么 $set 也就没有必要了(干掉 $set 方法本身也是性能的一个体现)。