0


在Vue3.0中,为什么放弃了Object.defineProperty,而使用Proxy来实现数据劫持?

在Vue3.0中,为什么放弃了Object.defineProperty,而使用Proxy来实现数据劫持?

前言

最近在复习Vue,对比着来学习记录~

正文

在解释问题之前,我们先回顾一下Proxy和Object.defineProperty的相关知识

1.1 Proxy

什么是Proxy?

Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。 可以理解成在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。

语法:

const p =newProxy(target, handler)
  • target:要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
  • handler:一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为

注意:

Proxy.revocable(target, handler);

用来创建一个可撤销的代理对象,其返回一个包含了代理对象本身和它的撤销方法revoke的可撤销 Proxy 对象。

一旦某个代理对象被撤销,它将变得几乎完全不可调用,在它身上执行任何的可代理操作都会抛出 TypeError 异常(注意,可代理操作一共有 14 种,执行这 14 种操作以外的操作不会抛出异常)。一旦被撤销,这个代理对象便不可能被直接恢复到原来的状态,同时和它关联的目标对象以及处理器对象都有可能被垃圾回收掉

var revocable = Proxy.revocable({},{get(target, name){return"[["+ name +"]]";}});var proxy = revocable.proxy;
proxy.foo;// "[[foo]]"

revocable.revoke();

console.log(proxy.foo);// 抛出 TypeError
proxy.foo =1// 还是 TypeErrordelete proxy.foo;// 又是 TypeErrortypeof proxy            // "object",因为 typeof 不属于可代理操作

handler

get()
用于拦截对象的读取属性操作。

get:function(target, property, receiver){}

该方法会拦截目标对象的以下操作:

  • 访问属性: proxy[foo]proxy.bar
  • 访问原型链上的属性: Object.create(proxy)[foo]
  • Reflect.get()
var p =newProxy({},{get:function(target, prop, receiver){
    console.log("called: "+ prop);return10;}});

console.log(p.a);// "called: a"// 10

set()
设置属性值操作的捕获器

set:function(target, property, value, receiver){}

该方法会拦截目标对象的以下操作:

  • 指定属性值:proxy[foo] = bar 和 proxy.foo = bar
  • 指定继承者的属性值:Object.create(proxy)[foo] = bar
  • Reflect.set()
var p =newProxy({},{set:function(target, prop, value, receiver){
    target[prop]= value;
    console.log('property set: '+ prop +' = '+ value);returntrue;}})

console.log('a'in p);// false

p.a =10;// "property set: a = 10"
console.log('a'in p);// true
console.log(p.a);// 10

对一个空对象架设了一层拦截,实际上像重载(overload)了点运算符,即用自己的定义覆盖了语言的原始定义。

var obj =newProxy({},{get(target,prop){
        console.log(`读取了${prop}`);return Reflect.get(target, prop);},set(target,prop,value){
        console.log(`更新了${prop}`);return Reflect.set(target, prop, value);}})
obj.a=1;//更新了a
console.log(obj.a);//读取了a// 1
注意

要使得Proxy起作用,必须针对Proxy实例(上例是proxy对象)进行操作,而不是针对目标对象(上例是空对象)进行操作。


handler 对象的其他方法:

  • apply方法拦截函数的调用、call和apply操作。
  • has()方法用来拦截HasProperty操作,即判断对象是否具有某个属性时,这个方法会生效。典型的操作就是in运算符。虽然for…in循环也用到了in运算符,但是has()拦截对for…in循环不生效。
  • deleteProperty(target, propKey):拦截delete proxy[propKey]的操作,返回一个布尔值。
  • ownKeys(target):拦截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for…in循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。
  • getOwnPropertyDescriptor(target, propKey):拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。 defineProperty(target, propKey, propDesc):拦截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一个布尔值。
  • preventExtensions(target):拦截Object.preventExtensions(proxy),返回一个布尔值。
  • getPrototypeOf(target):拦截Object.getPrototypeOf(proxy),返回一个对象。
  • isExtensible(target):拦截Object.isExtensible(proxy),返回一个布尔值。
  • setPrototypeOf(target, proto):拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。
  • construct(target, args):拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(…args)。
  • defineProperty():用于拦截对对象的 Object.defineProperty() 操作。

1.2 Object.defineProperty()

定义与语法

Object.defineProperty()方法的作用?

Object.defineProperty()

方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。

语法:

Object.defineProperty(obj, prop, descriptor)
  • obj:要定义属性的对象。
  • prop:要定义或修改的属性的名称或 Symbol 。
  • descriptor:要定义或修改的属性描述符。属性描述符有两种主要形式:数据描述符和存取描述符。数据描述符是一个具有值的属性,该值可以是可写的,也可以是不可写的。存取描述符是由 getter 函数和 setter 函数所描述的属性。

描述符

  • value表示该属性对应的值。默认为 undefined
  • writable 属性设置为 false 时,该属性被称为“不可写的”。它不能被重新赋值。试图写入非可写属性不会改变它,也不会引发错误默认值是false
  • enumerable 定义了对象的属性是否可以在 for…in 循环和 Object.keys() 中被枚举。默认值是false
  • configurable 特性表示对象的属性是否可以被删除,以及除 value 和 writable 特性外的其他特性是否可以被修改。默认值是false
存取描述符

还具有以下可选键值:

  • get:属性的 getter 函数,如果没有 getter,则为 undefined。当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入 this 对象(由于继承关系,这里的this并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值。默认为 undefined。
  • set:属性的 setter 函数,如果没有 setter,则为 undefined。当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象。默认为 undefined。

对于

数据描述符

存取描述符

,其差别如下:
在这里插入图片描述

使用

如果对象中不存在指定的属性,Object.defineProperty() 会创建这个属性。当描述符中省略某些字段时,这些字段将使用它们的默认值。如果属性已经存在,Object.defineProperty()将尝试根据描述符中的值以及对象当前的配置来修改这个属性。

var o ={};// 创建一个新对象// 在对象中添加一个属性与数据描述符的示例
Object.defineProperty(o,"a",{value:37,writable:true,enumerable:true,configurable:true});// 对象 o 拥有了属性 a,值为 37// 在对象中添加一个设置了存取描述符属性的示例var bValue =38;
Object.defineProperty(o,"b",{// 使用了方法名称缩写(ES2015 特性)// 下面两个缩写等价于:// get : function() { return bValue; },// set : function(newValue) { bValue = newValue; },get(){return bValue;},set(newValue){ bValue = newValue;},enumerable:true,configurable:true});
console.log(o.a);// 37
console.log(o.b);// 38

o.a=100
bValue=9;
console.log(o.a);// 100
console.log(o.b);// 9
注意

直接在get函数中return o.b的话,这里的o.b同时也会调用一次get函数,这样的话会陷入一个死循环;set函数也是同样的道理,因此我们通过一个第三方的变量bValue来防止死循环。
在这里插入图片描述

使用点运算符和 Object.defineProperty() 为对象的属性赋值时,数据描述符中的属性默认值是不同的

var o ={};

o.a =1;// 等同于:
Object.defineProperty(o,"a",{value:1,writable:true,configurable:true,enumerable:true});// 另一方面,
Object.defineProperty(o,"a",{value:1});// 等同于:
Object.defineProperty(o,"a",{value:1,writable:false,configurable:false,enumerable:false});

如果访问者的属性是被继承的,它的 get 和 set 方法会在子对象的属性被访问或者修改时被调用。如果这些方法用一个变量存值,该值会被所有对象共享。

functionmyclass(){}var value;
Object.defineProperty(myclass.prototype,"x",{get(){return value;},set(x){
    value = x;}});var a =newmyclass();var b =newmyclass();
a.x =1;
console.log(b.x);// 1

这可以通过将值存储在另一个属性中解决。在 get 和 set 方法中,this 指向某个被访问和修改属性的对象。

functionmyclass(){}

Object.defineProperty(myclass.prototype,"x",{get(){returnthis.stored_x;},set(x){this.stored_x = x;}});var a =newmyclass();var b =newmyclass();
a.x =1;
console.log(b.x);// undefined

1.3 分析

Vue2

//源数据let person ={name:'张三',age:18}let p ={}
Object.defineProperty(p,'name',{configurable:true,get(){//有人读取name时调用return person.name
    },set(value){//有人修改name时调用
        console.log('有人修改了name属性,我发现了,我要去更新界面!')
        person.name = value
    }})
  • 对于对象属性,我们使用Object.defineProperty对属性的读写、修改进行拦截(数据劫持)
  • 对于数组类型,通过重写更新数组的一系列方法来实现拦截

这样做的缺点是:

  • 虽然Object.defineProperty能够劫持对象的属性,但是需要对data对象的每一个属性进行遍历劫持;
  • Vue 无法检测 property 的添加或移除。由于 Vue 会在初始化实例时对 property 执行 getter/setter 转化,所以 property 必须在最开始时在 data 对象上存在才能让 Vue 将它转换为响应式的。
// 监测不到this.person.sex=女
deletethis.person.name

// 监测得到this.$set(this.person,'sex',女)// Vue.set(this.person,'sex',女)this.$delete(this.person.'name')//Vue.delete(this.person.'name')
  • Vue 不能检测到你利用索引直接设置一个数组项,例如:vm.items[indexOfItem] = newValue。也不能检测到你修改数组的长度时,例如:vm.items.length = newLength。
// 监测不到this.person.hobby[0]='学习'// 监测得到this.$set(this.person.hobby,0,'逛街')this.person.hobby.splice(0,1,splice)

Vue3

//源数据let person ={name:'张三',age:18}const p =newProxy(person,{//有人读取p的某个属性时调用get(target,propName){
        console.log(`有人读取了p身上的${propName}属性`)return Reflect.get(target,propName)},//有人修改p的某个属性、或给p追加某个属性时调用set(target,propName,value){
        console.log(`有人修改了p身上的${propName}属性,我要去更新界面了!`)
        Reflect.set(target,propName,value)},//有人删除p的某个属性时调用deleteProperty(target,propName){
        console.log(`有人删除了p身上的${propName}属性,我要去更新界面了!`)return Reflect.deleteProperty(target,propName)}})

使用Proxy对比Object.defineProperty做数据劫持好处如下:

  • 相较于Object.defineProperty劫持某个属性,Proxy则更彻底,不在局限某个属性,而是直接对整个对象进行代理。
  • Proxy能够监听到对象属性的增加、删除。
// 检查得到
person.sex=女
delete person.name
  • 不管是数组下标或者数组长度的变化,还是通过函数调用,Proxy都能很好的监听到变化;而且除了我们常用的get、set,Proxy更是支持13种拦截操作。
// 可检测
person.hobby[0]='学习'

本篇文章到处结束,如果觉得对你有帮助,记得收藏~

标签: vue es6 javascript

本文转载自: https://blog.csdn.net/qq_45890970/article/details/123992485
版权归原作者 Concise200 所有, 如有侵权,请联系我们删除。

“在Vue3.0中,为什么放弃了Object.defineProperty,而使用Proxy来实现数据劫持?”的评论:

还没有评论