proxy和defineproperty区别深度比较以及vue2、vue3中的使用

108 阅读5分钟

Proxy 和 Object.defineProperty 是两种不同的机制,它们各自有其特点和应用场景。以下是两者之间的深度比较:

Object.defineProperty

优点
  1. 细粒度控制

    • Object.defineProperty 允许你单独定义对象的某个属性的行为,包括读取、写入、枚举和配置等。
    • 可以设置属性的 get 和 set 方法,从而在属性被访问或修改时触发特定的逻辑。
  2. 向后兼容

    • Object.defineProperty 是在 ES5 中引入的,因此在较旧的环境中也有很好的支持。
缺点
  1. 单一属性控制

    • 只能针对单个属性进行拦截,无法对整个对象的操作进行拦截。
    • 如果需要对多个属性进行拦截,需要为每个属性分别调用 Object.defineProperty
  2. 无法监听新增属性

    • 对于新添加的属性,除非再次调用 Object.defineProperty,否则无法对这些属性进行拦截。
  3. 数组操作限制

    • 对于数组的操作,如 pushpop 等方法,Object.defineProperty 无法直接拦截这些操作。

Proxy

优点
  1. 全面拦截

    • Proxy 可以拦截整个对象的所有操作,包括属性的读取、写入、删除、枚举等。
    • 还可以拦截到 in 操作符、for...in 循环、Object.keys 等方法。
  2. 数组操作支持

    • Proxy 可以拦截数组的操作,如 pushpopshiftunshift 等,使得对数组的变更也能被监控。
  3. 灵活的拦截器

    • Proxy 提供了多种拦截器方法,如 getsetapplyconstruct 等,可以实现更复杂的逻辑。
  4. 新特性支持

    • Proxy 是 ES6 引入的新特性,支持更现代的 JavaScript 语法和特性。
缺点
  1. 浏览器兼容性

    • Proxy 在一些较老的浏览器中可能不被支持,需要 polyfill 才能使用。
  2. 性能开销

    • 使用 Proxy 可能会有一定的性能开销,尤其是在频繁访问对象属性的情况下。

深度比较

  • 拦截范围

    • Object.defineProperty 只能对单个属性进行拦截,而 Proxy 可以对整个对象的操作进行拦截。
    • Proxy 能够拦截更多的操作,如 hasownKeysgetOwnPropertyDescriptor 等。
  • 数组操作

    • Object.defineProperty 仅能拦截数组的 [index] 访问和 length 属性的变化,而 Proxy 可以拦截数组的所有操作,如 pushpop 等。
  • 配置复杂度

    • 使用 Object.defineProperty 需要为每个需要监控的属性单独设置,而 Proxy 只需创建一次即可监控整个对象。
  • 性能

    • Proxy 可能比 Object.defineProperty 更耗费性能,因为它需要处理所有的访问和修改。
  • 兼容性

    • Object.defineProperty 在 ES5 中就已存在,而 Proxy 是 ES6 的特性,可能在某些旧环境中需要 polyfill。

使用场景

  • 数据绑定

    • Vue.js 2.x 使用 Object.defineProperty 实现响应式系统,而 Vue.js 3.x 开始使用 Proxy 来提升性能和简化实现。
  • 框架内部

    • 在现代前端框架中,如 React、Vue 等,使用 Proxy 可以更方便地实现数据的响应式机制。

Object.defineProperty 示例

假设我们要创建一个对象,并且希望当访问或修改某个属性时能够触发特定的行为。

let person = {};

// 使用 Object.defineProperty 定义一个名为 'name' 的属性
Object.defineProperty(person, 'name', {
  get: function() {
    console.log('Getting name');
    return this._name;
  },
  set: function(value) {
    console.log('Setting name to ' + value);
    this._name = value;
  },
  enumerable: true,
  configurable: true
});

person.name = 'Alice';  // 输出: Setting name to Alice
console.log(person.name); // 输出: Getting name
                          // 输出: Alice

在这个例子中,我们定义了一个 name 属性,当访问或修改这个属性时,都会触发相应的 get 和 set 方法。

Proxy 示例

如果我们想要对整个对象的操作进行拦截,可以使用 Proxy。下面是一个简单的例子,展示了如何使用 Proxy 来拦截对象的操作。

let target = { count: 0 };

// 创建一个代理对象
let handler = {
  get(target, propKey, receiver) {
    console.log(`Getting ${propKey}`);
    return Reflect.get(target, propKey, receiver);
  },
  set(target, propKey, value, receiver) {
    console.log(`Setting ${propKey} to ${value}`);
    return Reflect.set(target, propKey, value, receiver);
  }
};

let proxy = new Proxy(target, handler);

proxy.count = 1;  // 输出: Setting count to 1
console.log(proxy.count); // 输出: Getting count
                         // 输出: 1

在这个例子中,我们创建了一个 Proxy 对象,它包装了一个普通的对象 target。我们定义了一个处理器对象 handler,其中包含了 get 和 set 方法,这两个方法会在访问和修改 target 对象的属性时被调用

Vue 2 响应式系统(使用 Object.defineProperty

基本原理

Vue 2 使用 Object.defineProperty 来实现数据的响应式。具体步骤如下:

  1. 数据劫持:在组件初始化时,通过递归遍历数据对象,使用 Object.defineProperty 来劫持每个属性。
  2. 依赖收集:在渲染过程中,通过观察者模式收集依赖。
  3. 依赖更新:当数据发生变化时,通知观察者进行更新。
源码示例
function observe(value) {
  if (!isObject(value)) {
    return;
  }
  let observer = new Observer(value);
  return observer;
}

class Observer {
  constructor(value) {
    this.value = value;
    this.walk(value);
  }

  walk(obj) {
    const keys = Object.keys(obj);
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i]);
    }
  }
}

function defineReactive(obj, key, val) {
  let dep = new Dep();

  Object.defineProperty(obj, key, {
    get() {
      Dep.target && dep.addDep(Dep.target);
      return val;
    },
    set(newVal) {
      if (newVal === val) return;
      val = newVal;
      dep.notify();
    }
  });
}

class Dep {
  constructor() {
    this.deps = [];
  }

  addDep(dep) {
    this.deps.push(dep);
  }

  notify() {
    this.deps.forEach(dep => dep.update());
  }
}

class Watcher {
  constructor(vm, expOrFn, cb) {
    this.vm = vm;
    this.cb = cb;
    this.expOrFn = expOrFn;
    this.value = this.get();
  }

  get() {
    Dep.target = this;
    const value = this.expOrFn.call(this.vm, this.vm);
    Dep.target = null;
    return value;
  }

  update() {
    this.run();
  }

  run() {
    const value = this.get();
    this.cb.call(this.vm, value, this.value);
    this.value = value;
  }
}

Vue 3 响应式系统(使用 Proxy

基本原理

Vue 3 使用 Proxy 来实现数据的响应式。具体步骤如下:

  1. 数据劫持:在组件初始化时,通过 Proxy 来劫持数据对象的所有操作。
  2. 依赖收集:在渲染过程中,通过 effect 函数来收集依赖。
  3. 依赖更新:当数据发生变化时,通过 trigger 函数来通知依赖进行更新。
源码示例
function reactive(target) {
  return new Proxy(target, {
    get(target, key, receiver) {
      track(target, key);
      return Reflect.get(target, key, receiver);
    },
    set(target, key, value, receiver) {
      let oldValue = target[key];
      let result = Reflect.set(target, key, value, receiver);
      if (oldValue !== value) {
        trigger(target, key);
      }
      return result;
    }
  });
}

const activeEffect = null;

function effect(fn, options = {}) {
  const effectFn = () => {
    cleanup(effectFn);
    activeEffect = effectFn;
    fn();
    activeEffect = null;
  };
  effectFn.deps = [];
  if (!options.lazy) {
    effectFn();
  }
  return effectFn;
}

function track(target, key) {
  if (!activeEffect) return;
  let depsMap = getDepMap(target);
  let dep = depsMap.get(key);
  if (!dep) {
    dep = new Set();
    depsMap.set(key, dep);
  }
  dep.add(activeEffect);
  activeEffect.deps.push(dep);
}

function trigger(target, key) {
  const depsMap = getDepMap(target);
  const dep = depsMap.get(key);
  if (dep) {
    dep.forEach(effectFn => {
      if (effectFn !== activeEffect) {
        queueJob(effectFn);
      }
    });
  }
}

function getDepMap(target) {
  let depsMap = target.__v_isReactive__ ? target : target.__v_reactive__;
  if (!depsMap) {
    depsMap = new WeakMap();
    target.__v_reactive__ = depsMap;
  }
  return depsMap;
}

function cleanup(effectFn) {
  for (let i = 0; i < effectFn.deps.length; i++) {
    let dep = effectFn.deps[i];
    dep.delete(effectFn);
  }
  effectFn.deps.length = 0;
}

function queueJob(job) {
  queue.push(job);
  if (!isFlushing) {
    isFlushing = true;
    Promise.resolve().then(() => {
      isFlushing = false;
      queue.forEach(job => job());
      queue = [];
    });
  }
}

let queue = [];
let isFlushing = false;

// 示例使用
const state = reactive({ count: 0 });

effect(() => {
  console.log('Rendering', state.count);
});

state.count = 1;  // 输出: Rendering 1
state.count = 2;  // 输出: Rendering 2

总结

  • Vue 2 使用 Object.defineProperty 实现响应式系统,适用于需要细粒度控制的场景。
  • Vue 3 使用 Proxy 实现响应式系统,适用于需要全面拦截和优化性能的场景。

选择哪种方式取决于具体的应用需求和环境。在现代前端开发中,Vue 3 的 Proxy 实现提供了更强大的功能和更好的性能表现。