Object.defineProperty和Proxy比较

2021/6/28 JavaScript

参考文章:

Object.defineProperty是数据劫持,Proxy是代理。

# 实现

# vue2的defineProperty实现

const data = { name: 'xxx' };

function defineReactive(data, key, val) {
  observe(val); // 监听子属性
  Object.defineProperty(data, key, {
    configurable: true, // 属性可删除
    enumerable: true, // 可枚举
    get() {
      return val;
    },
    set(newVal) {
      console.log('set', val, '-->', newVal);
      val = newVal;
    },
  });
}

function observe(data) {
  if (!data || typeof data !== 'object') {
    return;
  }
  // 取出所有属性遍历
  Object.keys(data).forEach((key) => {
    defineReactive(data, key, data[key]);
  });
}

observe(data);

data.name = 'OOO'; // set xxx --> OOO
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

# vue3的Proxy实现

const data = { name: 'xxx' };

const handler = {
  get(target, key) {
    console.log(`${key} 被读取`);
    return Reflect.get(target, key);
  },
  set(target, key, value) {
    console.log(`${key} 被设置为 ${value}`);
    Reflect.set(target, key, value);
  },
};

function observe(data) {
  if (!data || typeof data !== 'object') return false;
  // 如果对象的属性还是引用值,那么还是需要递归的去监听
  const proxyData = new Proxy(data, handler);
  return proxyData;
}

const proxyData = observe(data);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 为什么Vue3用Proxy性能更好

  1. Proxy可以直接监听数组的变化;
  2. Proxy可以监听对象而非属性。它在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy直接可以劫持整个对象,并返回一个新对象。
  3. Proxy对代理对象的监听更加丰富。有且不限于以下场景:Proxy可以监听对象新属性的定义(construct)和函数操作的调用(apply)、ownKeysdeletePropertyhas,而Object.defineProperty无法实现。

# Object.defineProperty和Proxy的缺点是什么

# Object.defineProperty的缺点

  • 无法检测到对象属性的新增或删除,如果有新增属性需要实现双向绑定则需要手动调用Vue.set()/vm.$set();只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历,如果属性值也是对象那么需要深度遍历。
  • 无法监听数组变化,文档所说的能够监听七种数组操作(pushpopunshiftshiftreversesortsplice),这都是作者重写了这些方法。
// src/core/observer/array.js

// 获取数组的原型Array.prototype,上面有我们常用的数组方法
const arrayProto = Array.prototype;
// 创建一个空对象arrayMethods,并将arrayMethods的原型指向Array.prototype
export const arrayMethods = Object.create(arrayProto);

// 列出需要重写的数组方法名
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse',
];
// 遍历上述数组方法名,依次将上述重写后的数组方法添加到arrayMethods对象上
methodsToPatch.forEach((method) => {
  // 保存一份当前的方法名对应的数组原始方法
  const original = arrayProto[method];
  // 将重写后的方法定义到arrayMethods对象上,function mutator() {}就是重写后的方法
  def(arrayMethods, method, function mutator(...args) {
    // 调用数组原始方法,并传入参数args,并将执行结果赋给result
    const result = original.apply(this, args);
    // 当数组调用重写后的方法时,this指向该数组,当该数组为响应式时,就可以获取到其__ob__属性
    const ob = this.__ob__;
    let inserted;
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args;
        break;
      case 'splice':
        inserted = args.slice(2);
        break;
    }
    // 如果有新增元素,则给新增的元素添加监听
    if (inserted) ob.observeArray(inserted);
    // 将当前数组的变更通知给其订阅者
    ob.dep.notify();
    // 最后返回执行结果result
    return result;
  });
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
  • 对于响应式数组,当浏览器支持__proto__属性时,使用push等方法时先从其原型arrayMethods上寻找push方法,也就是重写后的方法,处理之后数组的变化会通知到其订阅者,更新页面,当在arrayMethods上查询不到时会向上在Array.prototype上查询;当浏览器不支持__proto__属性时,使用push等方法时会先从数组自身上查询,如果查询不到会向上再Array.prototype上查询。
  • 对于非响应式数组,当使用push等方法时会直接从Array.prototype上查询。

# Proxy的缺点

  • Proxy的劣势就是兼容性问题,而且无法用polyfill实现。

# Object.defineProperty是一种怎样的机制

  • 无需显示调用: 例如Vue运用数据劫持+发布订阅,直接可以通知变化并驱动视图。
  • 可精确得知变化数据:劫持了属性的setter,当属性值改变,我们可以精确获知变化的内容newVal,因此在这部分不需要额外的diff操作,否则我们只知道数据发生了变化而不知道具体哪些数据变化了,这个时候需要大量diff来找出变化值,这是额外性能损耗。

# 题外话

其他能够监听对象属性变化的方法

  • Object.observe() 方法用于异步地监视一个对象的修改。当对象属性被修改时,方法的回调函数会提供一个有序的修改流。然而,这个接口已经被废弃并从各浏览器中移除。你可以使用更通用的 Proxy 对象替代。

  • Object.prototype.watch()方法会监视属性是否被赋值并在赋值时运行相关函数。

    警告: 通常来讲,你应该尽量避免使用 watch()unwatch() 这两个方法。因为只有 Gecko 实现了这两个方法,并且它们主要是为了在调试方便。另外,使用 watchpoint 对性能有严重的负面影响,在全局对象(如 window)上使用时尤其如此。你可以使用 setters and getters 或者 Proxy 代替。参见 Compatibility (opens new window) 了解详情。

最近更新: 2025年02月28日 14:53:18