相信大家在谈到数据拦截这一方面都不会陌生,js为我们提供了两种数据拦截的方式
- Object.defineProperty
- Proxy
这两种方式分别用于vue2和vue3中的响应式,那么这两种方式有什么区别呢,为什么vue3会使用proxy的方式更新响应式呢?这篇我们就深度来了解一下
拦截的方式
所谓数据拦截,无外乎就是你在对数据进行操作,例如读数据、写数据的时候
ini
const obj = {name : "张三"};
obj.name; // 正常读数据,直接就读了
obj.name = "李四"; // 正常写数据,直接就写了
obj.age = 18;
我们需要一种机制,在读写操作的中途进行一个打断,从而方便做一些额外的事情。这种机制我们就称之为数据拦截。其实更像是一种中间件,在数据完成之前对数据抽离出来做一些处理
这种拦截打断的场景其实有很多,比如 Vue 或者 React 里面的生命周期钩子方法,这种钩子方法本质上也是一种拦截,在组件从初始化到正常渲染的时间线里,设置了几个拦截点,从而方便开发者做一些额外的事情。
那么简单介绍一下拦截的两个api
Object.defineProperty
这是 Object 上面的一个静态方法,用于给一个对象添加新的属性 ,除此之外还能够对该属性进行更为详细的配置。
css
Object.defineProperty(obj, prop, descriptor)
- obj :要定义属性的对象
- prop:一个字符串或
Symbol
,指定了要定义或修改的属性键。 - descriptor:属性描述符。
重点其实是在属性描述符,这个参数是一个对象,可以描述的信息有:
-
value 设置属性值,默认值为 undefined.
-
writable 设置属性值是否可写,默认值为 false.
-
enumerable 设置属性是否可枚举,默认为 false.
-
configurable 是否可以配置该属性,默认值为 false. 这里的配置主要是针对这么一些点:
- 该属性的类型是否能在数据属性和访问器属性之间更改
- 该属性是否能删除
- 描述符的其他属性是否能被更改
-
get 取值函数,默认为 undefined.
-
set 存值函数,默认为 undefined
数据属性:value、writable
访问器属性:getter、setter
数据属性和访问器属性默认是互斥。
也就是说,默认情况下,使用 Object.defineProperty( ) 添加的属性是不可写、不可枚举和不可配置的。 这里我们引用一下mdn的描述 具体的可以到mdn上进行详细查看
ini
function Student() {
let stuName = "张三";
Object.defineProperty(this, "name", {
get() {
return stuName;
},
set(value) {
if (!isNaN(value)) {
stuName = "张三";
} else {
stuName = value;
}
},
});
}
const stu = new Student();
console.log(stu.name);
stu.name = "李四";
console.log(stu.name);
stu.name = 100;
console.log(stu.name);
2.Proxy
另外一种方式是使用 Proxy. 这是 ES6 新提供的一个 API,通过创建代理对象的方式来实现拦截 。 详细查看mdn
javascript
const p = new Proxy(target, handler)
- target : 目标对象,可以是任何类型的对象,包括数组,函数。
- handler: 定义代理对象的行为。
- 返回值:返回的就是一个代理对象,之后外部对属性的读写都是针对代理对象来做的
javascript
function Student() {
const obj = {
name: "张三",
};
return new Proxy(obj, {
get(obj, prop) {
return obj[prop] + "是个好学生";
},
set(obj, prop, value) {
if (!isNaN(value)) {
obj[prop] = "张三";
} else {
obj[prop] = value;
}
},
});
}
const stu = new Student(); // stu 拿到的就是代理对象
console.log(stu.name); // 张三是个好学生
stu.name = "李四";
console.log(stu.name); // 李四是个好学生
stu.name = 100;
console.log(stu.name); // 张三是个好学生
两者的共同点
1 都可以进行对象的读取和写入的拦截
js
const obj = {};
let _data = "这是一些数据";
Object.defineProperty(obj, "data", {
get() {
console.log("读取data的操作被拦截了");
return _data;
},
set(value){
console.log("设置data的操作被拦截了");
_data = value;
}
});
obj.data = "这是新的数据";
console.log(obj.data);
js
const obj = {
data: "这是一些数据",
name: "张三"
};
const p = new Proxy(obj, {
get(obj, prop) {
console.log(`${prop}的读取操作被拦截了`);
return obj[prop];
},
set(obj, prop, value) {
// 前面相当于是拦截下这个操作后,我们要做的额外的操作
console.log(`${prop}的设置操作被拦截了`);
// 后面就是真实的操作
obj[prop] = value;
}
});
p.data = "这是新的数据";
p.name = "李四";
2 都可以进行深度的拦截 进行深度拦截但是需要手动进行递归
js
const data = {
level1: {
level2: {
value: 100,
},
},
};
function deepDefineProperty(obj) {
for (let key in obj) {
// 首先判断是否是自身属性以及是否为对象
if (obj.hasOwnProperty(key) && typeof obj[key] === "object") {
// 递归处理
deepDefineProperty(obj[key]);
}
// 缓存一下属性值
let _value = obj[key];
Object.defineProperty(obj, key, {
get() {
console.log(`读取${key}属性`);
return _value;
},
set(value) {
console.log(`设置${key}属性`);
_value = value;
},
configurable: true,
enumerable: true,
});
}
}
deepDefineProperty(data);
console.log(data.level1.level2.value);
console.log("----------------");
data.level1.level2.value = 200;
js
function deepProxy(obj) {
return new Proxy(obj, {
get(obj, prop) {
console.log(`读取了${prop}属性`);
if (typeof obj[prop] === "object") {
// 递归的再次进行代理
return deepProxy(obj[prop]);
}
return obj[prop];
},
set(obj, prop, value) {
console.log(`设置了${prop}属性`);
if (typeof value === "object") {
return deepProxy(value);
}
obj[prop] = value;
},
});
}
const proxyData = deepProxy(data);
console.log(proxyData.level1.level2.value);
console.log("----------------");
proxyData.level1.level2.value = 200;
两者的差异点
我们先来将一下Object.defineProperty与proxy在拦截这方面的区别
- Object.defineProperty 是针对对象特定属性 的读写操作 进行拦截,他无法对后续新增进来的数据进行拦截和处理,一旦后期给对象新增属性,是无法拦截到的,因为 Object.defineProperty 在设置拦截的时候是针对的特定属性,所以新增的属性无法被拦截。从写法上我们也可以看出来,每次拦截都是对对象中的某一个具体的属性进行拦截,相当于一个属性配备了一个拦截器,所以后续新增的属性没有配备拦截器,于是并没有办法监视得到
- Proxy 则是针对一整个对象 的多种操作 ,包括属性的读取、赋值、属性的删除、属性描述符的获取和设置、原型的查看、函数调用等行为 能够进行拦截。它是针对整个对象,后期哪怕新增属性也能够被拦截到。
于是,基于上述问题,vue3将原本的数据拦截方式替换为了proxy,但是在vue2中,针对这个问题vue独立封装了一套在Vue 2中,为了解决响应式系统的这些限制,Vue提供了一套全局API来处理这些特殊情况。这些API包括:
Vue.set
Vue.delete
this.$set
this.$delete
2. 性能上的区别
接下来是性能方面的区别,究竟哪种方式的性能更高呢?
大多数情况下,Proxy 是高效的,但是不能完全断定 Proxy 就一定比 Object.defineProperty 效率高,因为这还是得看具体的场景。
如果你需要拦截的操作类型较少,且主要集中在某些特定属性上,那么 Object.defineProperty 可能提供更好的性能。
- 但是只针对某个特定属性的拦截场景较少,一般都是需要针对一个对象的所有属性进行拦截
- 此时如果需要拦截的对象结构复杂(如需要递归到嵌套对象)或者需要拦截的操作种类繁多,那么使用这种方式就会变得复杂且效率低下。
如果你需要全面地拦截对象的各种操作,那么 Proxy 能提供更强大和灵活的拦截能力,尽管可能有一些轻微的性能开销。