深入理解两种数据拦截方式的区别

相信大家在谈到数据拦截这一方面都不会陌生,js为我们提供了两种数据拦截的方式

  1. Object.defineProperty
  2. 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在拦截这方面的区别

  1. Object.defineProperty 是针对对象特定属性读写操作 进行拦截,他无法对后续新增进来的数据进行拦截和处理,一旦后期给对象新增属性,是无法拦截到的,因为 Object.defineProperty 在设置拦截的时候是针对的特定属性,所以新增的属性无法被拦截。从写法上我们也可以看出来,每次拦截都是对对象中的某一个具体的属性进行拦截,相当于一个属性配备了一个拦截器,所以后续新增的属性没有配备拦截器,于是并没有办法监视得到
  2. Proxy 则是针对一整个对象多种操作 ,包括属性的读取、赋值、属性的删除、属性描述符的获取和设置、原型的查看、函数调用等行为 能够进行拦截。它是针对整个对象,后期哪怕新增属性也能够被拦截到。

于是,基于上述问题,vue3将原本的数据拦截方式替换为了proxy,但是在vue2中,针对这个问题vue独立封装了一套在Vue 2中,为了解决响应式系统的这些限制,Vue提供了一套全局API来处理这些特殊情况。这些API包括:

  1. Vue.set
  2. Vue.delete
  3. this.$set
  4. this.$delete

2. 性能上的区别

接下来是性能方面的区别,究竟哪种方式的性能更高呢?

大多数情况下,Proxy 是高效的,但是不能完全断定 Proxy 就一定比 Object.defineProperty 效率高,因为这还是得看具体的场景。

如果你需要拦截的操作类型较少,且主要集中在某些特定属性上,那么 Object.defineProperty 可能提供更好的性能

  • 但是只针对某个特定属性的拦截场景较少,一般都是需要针对一个对象的所有属性进行拦截
  • 此时如果需要拦截的对象结构复杂(如需要递归到嵌套对象)或者需要拦截的操作种类繁多,那么使用这种方式就会变得复杂且效率低下。

如果你需要全面地拦截对象的各种操作,那么 Proxy 能提供更强大和灵活的拦截能力,尽管可能有一些轻微的性能开销。

相关推荐
北岛寒沫几秒前
JavaScript(JS)学习笔记 1(简单介绍 注释和输入输出语句 变量 数据类型 运算符 流程控制 数组)
javascript·笔记·学习
everyStudy3 分钟前
JavaScript如何判断输入的是空格
开发语言·javascript·ecmascript
无心使然云中漫步1 小时前
GIS OGC之WMTS地图服务,通过Capabilities XML描述文档,获取matrixIds,origin,计算resolutions
前端·javascript
Bug缔造者1 小时前
Element-ui el-table 全局表格排序
前端·javascript·vue.js
xnian_2 小时前
解决ruoyi-vue-pro-master框架引入报错,启动报错问题
前端·javascript·vue.js
罗政2 小时前
[附源码]超简洁个人博客网站搭建+SpringBoot+Vue前后端分离
vue.js·spring boot·后端
麒麟而非淇淋3 小时前
AJAX 入门 day1
前端·javascript·ajax
2401_858120533 小时前
深入理解MATLAB中的事件处理机制
前端·javascript·matlab
阿树梢3 小时前
【Vue】VueRouter路由
前端·javascript·vue.js
随笔写4 小时前
vue使用关于speak-tss插件的详细介绍
前端·javascript·vue.js