关于Vue2的Object.defineProperty与Vue3.proxy的理解

216 阅读4分钟

面试的时候,对于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方法成本太高,耗性能):

  1. 当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue
  2. 当你修改数组的长度时,例如: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 的实现。它将检查当前运行的是哪个副作用,并将其与 targetproperty 记录在一起。这就是 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