Vue为何要用Proxy取代Object.defineProperty

2,046 阅读3分钟

vue2实现响应式的原理(数据驱动视图)是借助了ES5的 Object.defineProperty的get和set,但是它有一些缺陷,比如:

  • 一次性要把监听的数据递归到底,效率太低
  • 对新增的属性监听不到,无法实现响应式,需要借助Vue.$set
  • 对数组的push、pop等操作需要额外的代码才能实现响应式

而Proxy这个新增的API可以完美的解决上面3个问题,在介绍Proxy之前,先来了解一下Vue2是怎么实现数据响应式的。

Object.defineProperty存在的问题

一次性要把监听的数据递归到底,效率太低

const data = {
  path: "user",
  user: {
    name: "mickey",
    avator: "xxx.png"
  },
  list:['1'],
  detail:{}
};

function updateView() {
  console.log("update view");
}

function observer(data) {
  if (typeof data !== "object" || data == null) {
    return;
  }
  for (let key in data) {
    defineReactive(data, key, data[key]);
  }
}

function defineReactive(data, key, value) {
  observer(value);
  Object.defineProperty(data, key, {
    get() {
      return value;
    },
    set(newVal) {
      // 注意:这里也要深度监听
      observer(newVal);
      if (value !== newVal) {
        updateView();
        value = newVal;
      }
    }
  });
}
observer(data);
// 这个代码会在控制台打印一次updateview
data.path = "other";
// 这个代码也会在控制台打印一次updateview,因为在defineReactive函数的第一行实现了深度监听
data.user.name = "mickey_new";
// 会打印updateview
data.detail = {id:1,addr:'xxxx'}
// 会打印updateview,因为在set函数里也调用过了observer实现了深度监听
data.detail.id = 2

这里的代码并不是vue的真实源码,但足以说明vue实现响应式的原理。

值得注意的是,为了实现嵌套对象的响应式,需要在defineReactive和set内部都调用observer

理解了上面的代码就很容易看出来,如果data这个对象特别的复杂、嵌套结构很深,执行完 observer(data) 以后,会递归的把整个数据结构遍历一次,这样才能实现响应式。这就解释了上面说的Object.defineProperty的第一个缺陷,"一次性要把监听的数据递归到底,效率太低"

对新增的属性监听不到,无法实现响应式,需要借助Vue.$set

还是上面的代码,这个时候在data上面添加一个新属性

// 是不会在控制台打印updateview,因为在遍历data的时候的时候newProp属性还没有
data.newProp = 'new'

为了解决这个问题,可以模拟一个类似Vue.$set的方法来解决

function $set(target,propertyName,value){
  defineReactive(target,propertyName,value)
}

为了让新增的属性也实现响应式,调用一下$set

$set(data,'newProp')
// 这个时候可以打印 updateview了
data.newProp = 'new'

对数组的push、pop等操作需要额外的代码才能实现响应式

依旧是上面的代码,这个时候调用数组的push方法,不会触发响应式更新

// 不会打印updateview
data.list.push('2')

我们需要改写数组原生的方法

const originArrProto = Array.prorotype;
const arrProto = Object.create(originArrProto);
['push','pop','shift','unshift','slice','splice'].forEach(methodName => {
  arrProto[methodName] = () => {
    updateView()
  	originArrProto[methodName].call(this,...arguments);
  }
});

// 然后修改 observer 函数,添加对数据的判断
function observer(data) {
  if (typeof data !== "object" || data == null) {
    return;
  }
  if (Array.isArray(data)){
    data.__proto__ = arrProto;
  }
  for (let key in data) {
    defineReactive(data, key, data[key]);
  }
}

为了不污染全局的Array,上面的代码在 observer 函数中增加了判断,这样全局使用数组原生的push、pop等方法不受影响

上面这就是使用Object.defineProperty的三个缺陷的代码演示(只为了说明问题,具体实现可能和vue有出入),下面来看看用Proxy怎么解决这3个问题

Proxy实现响应式

Proxy是ES6+新增的一个API,通过代理来监听对数据的操作,具体的API可以自行搜索。为了实现和上述代码一样的功能(对象深度监听、对新增属性监听、对数据的原生方法监听),我们用Proxy很容易解决。

function reactive(data) {
  if (typeof data !== "object" || data == null) {
    return data;
  }
  const proxyConfig = {
    set(target, key, value, receiver) {
      const res = Reflect.set(target, key, value, receiver);
      updateView();
    },
    get(target, key, receiver) {
      const res = Reflect.get(target, key, receiver);
      // 这个地方深度监听
      return reactive(res);
    }
  };
  return new Proxy(data, proxyConfig);
}

function updateView() {
  console.log("update view");
}

const data = {
  path: "user",
  user: {
    name: "mickey",
    avator: "xxx.png"
  },
  list: ["1"],
  detail: {}
};

const proxyData = reactive(data);
proxyData.path = "other";
proxyData.user.name = "mickey_new";
proxyData.detail = { id: 1, addr: "xxxx" };
proxyData.detail.id = 2;
proxyData.newProp = "new";
proxyData.list.push("2");

上面对proxyData的修改都会打印update view

Proxy本身就可以察觉到新增属性和对数组的原生方法的调用,所以无需额外的代码就可以实现响应式。但是对于深度监听(也就是嵌套对象的监听),我们是在 get 方法里递归调用了 reactive 方法。这里需要强调的是,和vue2使用Object.defineProperty不同,Object.defineProperty是在一上来遍历整个数据结构来实现深度监听,这里用Proxy是在get的时候(访问属性时)才动态的去深度监听,所以Proxy在深度监听性能更好

所以,Proxy这个API可以完美的取代Object.defineProperty