提示💡
- Vue2不能监听数组下标变化(数组API重写),对象属性新增(set API)和删除(delete API),只能监听对象的属性,需要深度遍历。
- Vue3代理整个对象,IE8以下不支持,this指向代理对象。
Object. defineProperty
vue2.x 就是通过 Object.defineProperty 实现数据劫持,把 data 中绑定的属性全部转换成 getter()/setter(),然后结合订阅发布者模式实现响应式。getter()可以读取数据,收集依赖,setter()可以改写数据,在数据发生改变时发布消息通知订阅者,触发监听回调,更新视图。
响应式函数
定义一个响应式函数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()
}
}
定义一个响应式函数defineReactive
Object. defineProperty 的缺陷
- Object.defineProperty 不能监控数组下标的变化,导致通过数组下标添加的元素不能实时响应,并且- 数组
API
方法无法监听到。
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
- 无法劫持到对象的删除与添加属性操作。
const obj = {
foo: "foo",
bar: "bar"
}
observe(obj)
delete obj.foo // no ok
obj.jar = 'xxx' // no ok
- Object. defineProperty 只能监听对象的属性,导致需要对每个对象、每个属性都进行遍历,如果属性值也是对象,则需要进行深度遍历,造成性能问题。
Vue2
中,增加了 set
、delete
API,并且对数组 API 方法重写进行一个重写。
也可以使用 v-if
和 this.$forceUpdate()
进行强制刷新,但不推荐经常使用。
Proxy
在 Vue3.x 中使用了 Proxy 代替了 Object.defineProperty,因为它是对整个对象进行代理,所以可以监听对象某个属性值的变化,还可以监听对象属性的新增和删除,而且还可以监听数组。
注意❗
Proxy
不兼容 IE,也没有polyfill
,defineProperty
能支持到 IE 9 。
响应式方法
定义一个响应式方法 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
的缺点。
- 不能监控数组下标的变化
- 无法劫持到对象的删除与添加 属性操作。
- 只能监听对象的属性 ,需要深度遍历。
Vue2
中的兼容处理。
- 增加了
set
、delete
API - 对数组 API 方法重写进行一个重写。
- 也可以使用
v-if
和this.$forceUpdate()
进行强制刷新,但不推荐经常使用。
由上可知,Object.defineProperty
与 Proxy
的区别如下:
Object.defineProperty | Proxy | |
---|---|---|
定义 | 代理对象某个属性 | 代理整个对象 |
对象新增属性 | ❌ | ✅ |
数组新增修改和 length | ❌ | ✅ |
this | 指向目标对象 | 指向代理对象 |
兼容性 | 兼容 IE | 不兼容 IE8 及以下 |
使用上 | 考虑因素多 | 更方便,方法更多(13 种 handler ) |