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

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

前言

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

正文

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

1.1 Proxy

什么是Proxy?

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

语法:

const p = new Proxy(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           // 还是 TypeError
delete proxy.foo;       // 又是 TypeError
typeof proxy            // "object",因为 typeof 不属于可代理操作

handler

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

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

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

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

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 = new Proxy({}, {
  set: function(target, prop, value, receiver) {
    target[prop] = value;
    console.log('property set: ' + prop + ' = ' + value);
    return true;
  }
})

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 = new Proxy({},{
    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 方法会在子对象的属性被访问或者修改时被调用。如果这些方法用一个变量存值,该值会被所有对象共享。

function myclass() {
}

var value;
Object.defineProperty(myclass.prototype, "x", {
  get() {
    return value;
  },
  set(x) {
    value = x;
  }
});

var a = new myclass();
var b = new myclass();
a.x = 1;
console.log(b.x); // 1

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

function myclass() {
}

Object.defineProperty(myclass.prototype, "x", {
  get() {
    return this.stored_x;
  },
  set(x) {
    this.stored_x = x;
  }
});

var a = new myclass();
var b = new myclass();
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能够劫持对象的属性,但是需要对对象的每一个属性进行遍历劫持;

  • 无法检测到直接对对象属性的添加或删除,除非使用如下指令添加

// 监测不到
this.person.sex=delete this.person.name

// 监测得到
this.$set(this.person,'sex',)
// Vue.set(this.person,'sex',女)
this.$delete(this.person.'name')
//Vue.delete(this.person.'name')
  • 无法检测数组元素的变化,需要进行数组方法的重写
// 监测不到
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 = new Proxy(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]='学习'

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

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇

)">
下一篇>>