从根源理解Proxy对比defineProperty优势

6 阅读5分钟

从根源理解Proxy对比defineProperty优势

defineProperty和Proxy分别是Vue2和Vue3实现响应式的核心原理,实现数据响应式的关键在于对数据进行监听及操作,如以下对象obj,

let obj = {
    name: "木鱼",
    age: 18
};

当我们读取对象属性obj.name或修改对象属性obj.age=81,该如何监听到进行了obj属性的读取或修改操作的行为呢?思路是→把对象属性的读取和赋值变成一个函数,这样在函数实现读取或赋值的过程中顺便插入监听操作等逻辑代码即可实现数据截取、数据监听等功能

那么怎么把对象属性的读取赋值操作转变为一个函数呢?

Vue2实现思路:

Vue2采用了ES5提供的Object.defineProperty方法。

//接上片段代码
let obj2={}
Object.defineProperty(obj2,"name",{
    get(){
        console.log("调用了getter,则说明有人读取了name属性")
        return obj.name
    },
    set(value){
        console.log(`调用了setter,则说明有人修改了name属性为${value}`)
        obj.name=value
    }
})
console.log(obj2.name)//通过obj2拿到obj的name
obj2.name='沐萸'
console.log(obj.name)//通过修改obj2的name属性,obj的name也被同步修改

通过代理对象obj2即可对obj中的属性进行操作(即数据代理)。

那么怎么代理obj对象的所有属性呢?Vue2代码逻辑即为在一个observe函数中对obj对象进行深度遍历(利用for in对每个属性进行遍历,应用上述的Object.defineProperty逻辑代码,如果对象中有属性嵌套对象则进一步递归遍历执行)。

以上即为Vue2实现响应式数据的核心原理,Vue2通过类似上述的方式对定义的_data对象进行深度遍历,将_data对象里属性的读取赋值操作通过defineProperty都转变为一个函数(即getter、setter),并且都交给vm对象代理(vm对象即为所谓的代理对象obj2) ,因此对vm.name进行读取修改操作就相当于对vm._data.name进行操作,这样数据拦截、数据监听实现了,也好实现响应式数据啦~

然鹅。

Vue2实现方式的弊端:

Vue2无法监听属性的新增和删除。注意上述橙色字体尤其是加粗部分,这正是Vue2实现方式弊端的关键因素。由于它是针对每个属性进行监听,则要进行深度遍历,这会有效率的损失。而当执行了observe函数后,Vue2完成了深度遍历,_data中的数据属性都已经被改成getter、setter函数了,都被监听到了。但observe函数仅被执行了一次,这时再通过obj.newProp去新增数据对象的属性,由于observe函数已执行过,则不会再对后操作的obj新增的newProp属性进行转换为函数了,因此通过obj.newProp去新增数据对象的属性或去删除某个属性,Vue2是无法监听到该操作的。

Vue3解决Vue2弊端的关键:

Vue3同样需要把对象读取赋值转换为函数以实现监听,但Vue3不去监听对象的每个属性,而是直接监听整个对象→因此不需要进行深度遍历,只要对象变了Vue3即能监听到。

Vue3实现思路:

Vue3采用了ES6提供的Proxy方法(注意,由于该方法由ES6提供,说明Vue3也将不适用于不支持ES6以下版本的浏览器,这是Vue3的一个缺点)。

//接前面的obj对象
let handler = {
    get: function(target, prop, receiver) {
        console.log(`代理对象执行getter函数,获取目标对象的"${prop}"属性`);
        return target[prop];
    },
    set: function(target, prop, value, receiver) {
        console.log(`代理对象执行setter函数将目标对象的"${prop}"属性的值改为${value}`);
        target[prop] = value;
    }
};
let proxy = new Proxy(obj, handler);
console.log(proxy);
console.log(proxy.name); // 木鱼
console.log(proxy.age); // 18
proxy.age = 81;
console.log(proxy.age); // 81

image.png编辑执行结果如图,Proxy会将对象obj交由Proxy构造函数new出来的实例proxy代理,proxy中已被转化出带有get、set函数,则proxy能代理obj进行能被监听截取到的读取、赋值等操作。 proxy对比defineProperty的特别之处在于get和set函数带有形参prop(目标对象target的prop),这意味着无论是新增还是修改读取对象哪个属性,只要操作该代理对象,就会触发到该代理对象的get/set函数,这时相应被操作的属性则再通过形参prop传入get/set函数,从而实现对该对象属性的监听、拦截等操作(多层嵌套对象的监听则需在get/set函数中进一步递归子层对象target[prop]进行proxy代理,这里虽也有涉及递归,但只有要对该属性进行读取操作时才会执行到该递归操作,不会影响到一开始的效率)。删除属性则通过代理对象的deleteProperty函数实现(与get/set同理)。因此无需再像Vue2一样去深度遍历每一个属性再进行defineProperty操作。

通俗来讲,以多层嵌套对象mass为例,Vue2的observe函数是对mass的每个属性进行深度遍历,对每个属性进行defineProperty操作,使所有遍历到的属性都能通过get/set函数监听;而Vue3的observe函数是对mass这整个对象进行proxy操作,mass有哪个属性被操作则将该属性传入get/set/deleteProperty函数的形参prop中,从而能对该对象具体的属性操作行为进行截取/监听,而截取到的属性值如果是个对象则进行递归再次通过proxy实现对子层对象的监听。

因此Vue3的observe函数是通过proxy监听的是整个对象,无需去监听每个属性了,不需要像Vue2的observe函数一样对目标对象进行深度遍历,减少了性能的消耗;且之后对属性进行新增/删除操作,对象则会发生变化,Vue3是可以监听到的。 (Vue3的observe函数要监听多层嵌套对象,也是要用到递归的,)

理解了以上内容,最后再来看以下的八股文内容(是不是就简单且好记很多啦):

Vue3用Proxy代替defineProperty的原因:

响应式优化。

1.defineProperty API 的局限性最大原因是它只能针对单例属性做监听。

Vue2.x 中的响应式实现正是基于 defineProperty 中的 descriptor,对 data 中的属性做了遍历 + 递归,为每个属性设置了 getter、setter。

这也就是为什么 Vue 只能对 data 中预定义过的属性做出响应的原因,在 Vue 中使用下标的方式直接修改属性的值或者添加一个预先不存在的对象属性是无法做到 setter 监听的,这是 defineProperty 的局限性。

2. Proxy API 的监听是针对一个对象的,那么对这个对象的所有操作会进入监听操作,这就完全可以代理所有属性,将会带来很大的性能提升和更优的代码。

Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。

3. 响应式是惰性的

在 Vue.js 2.x 中,对于一个深层属性嵌套的对象,要劫持它内部深层次的变化,就需要递归遍历这个对象,执行 Object.defineProperty 把每一层对象数据都变成响应式的,这无疑会有很大的性能消耗

在 Vue.js 3.0 中,使用 Proxy API 并不能监听到对象内部深层次的属性变化,因此它的处理方式是在 getter 中去递归响应式,这样的好处是真正访问到的内部属性才会变成响应式,简单的可以说是按需实现响应式,减少性能消耗。