Object.defineProperty
Object.defineProperty()
方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象
Object.defineProperty
为什么能实现响应式
通过defineProperty
两个属性,get
及set
- get
属性的 getter 函数,当访问该属性时,会调用此函数。 执行时不传入任何参数。该函数的返回值会被用作属性的值。
- set
属性的 setter 函数,当属性值被修改时,会调用此函数。 该方法接受一个参数,默认返回 undefined。
下面通过代码展示:
定义一个响应式函数Reactive
function update() {
app.innerText = obj.foo
}
function Reactive(obj, key, val) {
Object.defineProperty(obj, key, {
get() {
console.log(`get ${key}:${val}`);
return val
},
set(newVal) {
if (newVal !== val) {
val = newVal
update()
}
}
})
}
调用Reactive
,数据发生变化触发update
方法,实现数据响应式
const obj = {}
Reactive(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 => {
Reactive(obj, key, obj[key])
})
}
如果存在嵌套对象的情况,还需要在defineReactive
中进行递归。
function Reactive(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()
}
}
})
}
上述例子能够实现对一个对象的基本响应式,但仍然存在诸多问题,例如对一个对象进行删除与添加属性操作,则会发现无法劫持到。
const obj = {
foo: "foo",
bar: "bar"
}
observe(obj)
delete obj.foo
obj.jar = 'xxx'
当我们对一个数组进行监听的时。数据的api
无法劫持到,从而无法实现数据响应式,
const arrData = [1,2,3,4,5];
arrData.forEach((val,index)=>{
defineProperty(arrData,index,val)
})
arrData.push()
arrData.pop()
arrDate[0] = 99
所以在Vue2
中,实现了一些额外的API,如Vue.set
和Vue.delete
来处理对象属性的动态添加和删除,并且对数组的一些方法进行了重写来触发响应式更新。
// 数组重写
//创建了一个新对象 `arrayProto`,继承 `Array.prototype`。
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')
当对象嵌套层级较深时,每一层对象的属性都需要通过defineProperty进行劫持,这会导致劫持操作的递归调用,使得性能受到影响。而且,Vue 2对于深层嵌套对象的监听是通过递归遍历对象的属性来实现的,这在数据量较大时会导致性能下降。
小结
- 检测不到对象属性的添加和删除
- 数组
API
方法无法监听到 - 需要对每个属性进行遍历监听,如果嵌套对象,需要深层监听,造成性能问题。
proxy
Proxy
的监听是针对一个对象的,那么对这个对象的所有操作会进入监听操作,这就完全可以代理所有属性了。
下面通过代码进行展示:
定义一个响应式方法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'
// 3.设置不存在属性
state.dong = 'dong'
// 4.删除属性
delete state.dong
再测试嵌套对象情况,发现不能劫持
const state = reactive({
bar: { a: 1 }
})
// 设置嵌套对象属性
state.bar.a = 10
如果要解决,需要在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
可以直接监听数组的变
const obj = [1,2,3,4]
const proxtObj = reactive(obj)
obj.psuh(5) // ok
Proxy
有多达13种拦截方法,不限于apply
、ownKeys
、deleteProperty
、has
等等,这是Object.defineProperty
不具备的