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

相信大家在谈到数据拦截这一方面都不会陌生,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 能提供更强大和灵活的拦截能力,尽管可能有一些轻微的性能开销。

相关推荐
进阶的小木桩14 分钟前
Vue 3 + Elementui + TypeScript 实现左侧菜单定位右侧内容
vue.js·elementui·typescript
暖木生晖5 小时前
flex-wrap子元素是否换行
javascript·css·css3·flex
gnip6 小时前
浏览器跨标签页通信方案详解
前端·javascript
gnip6 小时前
运行时模块批量导入
前端·javascript
逆风优雅7 小时前
vue实现模拟 ai 对话功能
前端·javascript·html
这是个栗子7 小时前
【问题解决】Vue调试工具Vue Devtools插件安装后不显示
前端·javascript·vue.js
姑苏洛言7 小时前
待办事项小程序开发
前端·javascript
Warren9810 小时前
公司项目用户密码加密方案推荐(兼顾安全、可靠与通用性)
java·开发语言·前端·javascript·vue.js·python·安全
1024小神11 小时前
vue3 + vite项目,如果在build的时候对代码加密混淆
前端·javascript
轻语呢喃12 小时前
useRef :掌握 DOM 访问与持久化状态的利器
前端·javascript·react.js