面试的时候,对于Object.defineProperty的回答总是很浅薄,今天查一查资料重新学习一下
vue2如何追踪变化
当你把一个普通的JavaScript对象传入Vue实例作为data选项,Vue将遍历此对象所有的property(属性和方法),并使用Object.defineProperty把这些property全部转换为getter/setter。
Object.defineProperty 是 ES5 中一个无法修正的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。
这些getter/setter对用户来说是不可见的,但是在内部他们让Vue能够追踪依赖,在property被访问和修改时通知变更。
每个组件实例都对应一个watcher实例, 它会在组件渲染的过程中把“接触”过的数据property记录为依赖,之后当依赖项的setter触发时,会通知watcher,从而使它关联的组件重新渲染。
检测变化的注意事项
由于JavaScript的限制,Vue不能检测数组和对象的变化。
这里说一下原始类型和引用类型:
- 原始类型的值存在于栈中,声名变量会在栈中开辟空间,赋值可以直接改变栈内存中的值。
- 引用类型是保存在堆内存中的对象,Javascript不允许直接访问堆内存的位置,因此也就不能直接操作对象所在的堆内存空间。在操作对象时,实际上操作的是该对象在栈中的引用。
对于对象
Vue 无法检测 property 的添加或移除。由于 Vue 会在初始化实例时对 property 执行 getter/setter 转化,所以 property 必须在 data 对象上存在才能让 Vue 将它转换为响应式的。例如:
var vm = new Vue({
data:{
a:1
}
})
// `vm.a` 是响应式的
vm.b = 2
// `vm.b` 是非响应式的
对于已经创建的实例,Vue 不允许动态添加根级别的响应式 property。但是,可以使用
$set方法向嵌套对象添加响应式 property(手动挂载getter和setter)。
如果要为已有对象赋值多个新 property呢?
你可能会这样写
Object.assign(this.someObject, {a: 1, b: 2})
但是,这样添加到对象上的新property不会触发更新。在这种情况在下,你应该用原对象
与要混合进去的对象的property一起创建一个新的对象。
this.someObject = Object.assign({}, this.someObject, {a: 1, b: 2})
对于数组
Vue 不能检测以下数组的变动(数组的数量可能很大,不断调用set方法成本太高,耗性能):
- 当你利用索引直接设置一个数组项时,例如:
vm.items[indexOfItem] = newValue - 当你修改数组的长度时,例如:
vm.items.length = newLength
解决这类问题,可以使用$set或splice
调用数组的pop、push、shift、unshift、splice、sort、reverse等方法时也是可以监听到数组的变化的
Object.defineProperty有什么缺点?
对引用类型的监听还是较为复杂,需要大量的手动处理,只能劫持对象的属性,,因此我们需要对每个对象的每个属性进行遍历,如果属性值也是对象那么需要深度遍历。
Vue3中的proxy
当我们从一个组件的data函数中返回一个普通的Javascript对象时,Vue会将该对象包裹在一个带有get和set处理程序的proxy中。
proxy是一个对象,它包装了另一个对象,并允许你拦截对该对象的任何操作
我们通常这样使用它:
const dinner = {
meal: 'tacos'
}
const handler = {
get(target, property) {
console.log("intercepted")
return target[property]
}
}
const proxy = new Proxy(dinner, handler)
console.log(proxy.meal)
这里我们截取了读取目标对象property的举动,像这样的处理函数也别称为一个捕捉器(trap)。有许多可用的不同类型的捕捉器,每个都处理不同类型的交互。
使用Proxy实现响应性的第一步就是跟踪一个property何时被读取,我们在一个名为track的处理器函数中执行此操作,该函数可以传入target和property两个参数。
const dinner = {
meal: 'tacos'
}
const handler = {
get(target, property, receiver) {
track(target, property)
return Reflect.get(...arguments)
}
}
const proxy = new Proxy(dinner, handler)
console.log(proxy.meal)
这里没有展示 track 的实现。它将检查当前运行的是哪个副作用,并将其与 target 和 property 记录在一起。这就是 Vue 如何知道这个 property 是该副作用的依赖项。
解释一下副作用: Vue通过一个副作用(effect)来追踪当前正在运行的函数。副作用是一个函数的包裹其,在函数被调用之前就启动跟踪。Vue知道哪个副作用在何时运行,并能在需要时再次执行它。
最后,我们需要在 property 值更改时重新运行这个副作用。为此,我们需要在代理上使用一个 set 处理函数:
const dinner = {
meal: 'tacos'
}
const handler = {
get(target, property, receiver) {
track(target, property)
return Reflect.get(...arguments)
},
set(target, property, value, receiver) {
trigger(target, property)
return Reflect.set(...arguments)
}
}
const proxy = new Proxy(dinner, handler)
console.log(proxy.meal)
// tacos
总结一下
1.当一个值被读取时进行追踪:proxy的get处理函数中track函数记录了该property和当前副作用。
2.当某个值改变时进行检测:在proxy上调用set处理函数。
3.重新运行代码来读取原始值:trigger函数查找哪些副作用依赖该property并执行它们。
注意:
Vue3中基本数据类型的响应式原理用的仍然是Object.defineProperty,引用类型的响应式用的才是proxy