一文了解Object.defineProperty()和Proxy

199 阅读2分钟

1. Object.defineProperty 的实现原理与限制

基本原理

Object.defineProperty 允许我们为对象的某个属性添加 getter 和 setter,当属性值被访问或修改时,可以拦截这些操作:

  • get:在访问属性时触发,通常用于依赖收集。
  • set:在修改属性时触发,通常用于派发更新。

以下是一个用 Object.defineProperty 实现简单响应式的示例:

function defineReactive(obj, key, value) {
  // 深度递归,确保子对象也能响应式
  if (typeof value === 'object' && value !== null) {
    observe(value);
  }
  
  // 定义 getter 和 setter
  Object.defineProperty(obj, key, {
    get() {
      console.log(`访问了属性 ${key}`);
      return value;
    },
    set(newValue) {
      if (newValue !== value) {
        console.log(`属性 ${key} 被修改为 ${newValue}`);
        value = newValue;
        // 如果新值是对象,则需要递归响应式
        if (typeof newValue === 'object' && newValue !== null) {
          observe(newValue);
        }
      }
    },
  });
}

function observe(obj) {
  if (typeof obj !== 'object' || obj === null) return;
  Object.keys(obj).forEach(key => defineReactive(obj, key, obj[key]));
}

// 测试
const data = { name: 'Vue', info: { version: '2.0' } };
observe(data);

data.name; // 访问了属性 name
data.name = 'Vue.js'; // 属性 name 被修改为 Vue.js
data.info.version; // 访问了属性 version
data.info.version = '2.1'; // 属性 version 被修改为 2.1

局限性

  1. 对象新增/删除属性不能被监听Object.defineProperty 只能作用于已存在的属性,新增属性必须使用 Vue.set
  2. 数组操作的监听不足:对数组的方法(如 pushsplice 等)无法直接拦截,Vue 2 通过覆盖数组原型方法解决。
  3. 需要递归遍历:深层嵌套对象需要递归遍历,每层属性都需要用 Object.defineProperty 包装,性能不佳。

Proxy 的实现原理与优势

基本原理

Proxy 是 ES6 引入的新特性,允许我们定义一个对象的基本操作行为(如读取、写入、删除等)的自定义逻辑,而不需要手动定义每个属性的 getter 和 setter。

  • 可以直接监听整个对象(包括新增/删除属性)。
  • 支持多种拦截操作(如 getsetdeleteProperty 等)。

以下是使用 Proxy 实现响应式的简单示例:

function reactive(obj) {
  return new Proxy(obj, {
    get(target, key) {
      const value = target[key];
      console.log(`访问了属性 ${key}`);
      // 如果是对象,递归响应式
      return typeof value === 'object' && value !== null ? reactive(value) : value;
    },
    set(target, key, value) {
      console.log(`属性 ${key} 被修改为 ${value}`);
      target[key] = value;
      // 可以在这里触发更新
      return true;
    },
    deleteProperty(target, key) {
      console.log(`属性 ${key} 被删除`);
      delete target[key];
      // 可以在这里触发更新
      return true;
    }
  });
}

// 测试
const data = reactive({ name: 'Vue', info: { version: '3.0' } });

data.name; // 访问了属性 name
data.name = 'Vue.js'; // 属性 name 被修改为 Vue.js
data.info.version; // 访问了属性 version
data.info.version = '3.1'; // 属性 version 被修改为 3.1
data.info.newKey = '新属性'; // 属性 newKey 被修改为 新属性
delete data.info.version; // 属性 version 被删除

优势

  1. 可以监听对象的新增/删除操作:不需要额外处理新增属性的问题。
  2. 无需递归Proxy 可以对整个对象的操作进行拦截,只有在访问时才递归处理子对象。
  3. 功能更强大:可以拦截更多的操作,比如数组的索引访问、in 操作符等。

Vue 的响应式实现对比

特性Object.defineProperty (Vue 2)Proxy (Vue 3)
新增/删除属性监听不支持,需手动处理原生支持
深度监听需要递归处理按需递归,性能更优
数组操作监听覆盖原型方法实现原生支持
API 灵活性较差更强大
浏览器支持较好需要现代浏览器支持

总结

  • 在 Vue 2 中,Object.defineProperty 是响应式系统的核心,但存在一些局限性,比如无法监听新增/删除属性,深度监听性能较差。
  • Vue 3 使用了 Proxy 重新实现响应式系统,解决了 Vue 2 的诸多问题,同时性能更好,代码更简洁。

这也是为什么 Vue 3 推荐使用现代浏览器,并将响应式设计从底层进行了重构的原因。