2.1 数据代理的含义

数据代理的另一个说法是数据劫持,当我们在访问或者修改对象的某个属性时,数据劫持可以拦截这个行为并进行额外的操作或者修改返回的结果。而我们知道Vue响应式系统的核心就是数据代理,代理使得数据在访问时进行依赖收集,在修改更新时对依赖进行更新,这是响应式系统的核心思路。而这一切离不开Vue对数据做了拦截代理。然而响应式并不是本节讨论的重点,这一节我们将看看数据代理在其他场景下的应用。在分析之前,我们需要掌握两种实现数据代理的方法:Object.definePropertyProxy

2.1.1 Object.defineProperty

官方定义:Object.defineProperty()方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。

基本用法:

  1. Object.defineProperty(obj, prop, descriptor)

Object.defineProperty()可以用来精确添加或修改对象的属性,只需要在descriptor对象中将属性特性描述清楚,descriptor的属性描述符有两种形式,一种是数据描述符,另一种是存取描述符,我们分别看看各自的特点。

  1. 数据描述符,它拥有四个属性配置
  • configurable:数据是否可删除,可配置
  • enumerable:属性是否可枚举
  • value:属性值,默认为undefined
  • writable:属性是否可读写
  1. 存取描述符,它同样拥有四个属性选项
  • configurable:数据是否可删除,可配置
  • enumerable:属性是否可枚举
  • get:一个给属性提供 getter 的方法,如果没有 getter 则为 undefined
  • set:一个给属性提供 setter 的方法,如果没有 setter 则为 undefined

需要注意的是: 数据描述符的value,writable 和 存取描述符中的get, set属性不能同时存在,否则会抛出异常。有了Object.defineProperty方法,我们可以方便的利用存取描述符中的getter/setter来进行数据的监听,这也是响应式构建的雏形。getter方法可以让我们在访问数据时做额外的操作处理,setter方法使得我们可以在数据更新时修改返回的结果。看看下面的例子,由于设置了数据代理,当我们访问对象oa属性时,会触发getter执行钩子函数,当修改a属性的值时,会触发setter钩子函数去修改返回的结果。

  1. var o = {}
  2. var value;
  3. Object.defineProperty(o, 'a', {
  4. get() {
  5. console.log('获取值')
  6. return value
  7. },
  8. set(v) {
  9. console.log('设置值')
  10. value = qqq
  11. }
  12. })
  13. o.a = 'sss'
  14. // 设置值
  15. console.log(o.a)
  16. // 获取值
  17. // 'qqq'

前面说到Object.definePropertygetset方法是对对象进行监测并响应变化,那么数组类型是否也可以监测呢,参照监听属性的思路,我们用数组的下标作为属性,数组的元素作为拦截对象,看看Object.defineProperty是否可以对数组的数据进行监控拦截。

  1. var arr = [1,2,3];
  2. arr.forEach((item, index) => {
  3. Object.defineProperty(arr, index, {
  4. get() {
  5. console.log('数组被getter拦截')
  6. return item
  7. },
  8. set(value) {
  9. console.log('数组被setter拦截')
  10. return item = value
  11. }
  12. })
  13. })
  14. arr[1] = 4;
  15. console.log(arr)
  16. // 结果
  17. 数组被setter拦截
  18. 数组被getter拦截
  19. 4

显然,已知长度的数组是可以通过索引属性来设置属性的访问器属性的。但是数组的添加确无法进行拦截,这个也很好理解,不管是通过arr.push()还是arr[10] = 10添加的数据,数组所添加的索引值并没有预先加入数据拦截中,所以自然无法进行拦截处理。这个也是使用Object.defineProperty进行数据代理的弊端。为了解决这个问题,Vue在响应式系统中对数组的方法进行了重写,间接的解决了这个问题,详细细节可以参考后续的响应式系统分析。

另外如果需要拦截的对象属性嵌套多层,如果没有递归去调用Object.defineProperty进行拦截,深层次的数据也依然无法监测。

2.1.2 Proxy

为了解决像数组这类无法进行数据拦截,以及深层次的嵌套问题,es6引入了Proxy的概念,它是真正在语言层面对数据拦截的定义。和Object.defineProperty一样,Proxy可以修改某些操作的默认行为,但是不同的是,Proxy针对目标对象会创建一个新的实例对象,并将目标对象代理到新的实例对象上,。 本质的区别是后者会创建一个新的对象对原对象做代理,外界对原对象的访问,都必须先通过这层代理进行拦截处理。而拦截的结果是我们只要通过操作新的实例对象就能间接的操作真正的目标对象了。针对Proxy,下面是基础的写法:

  1. var obj = {}
  2. var nobj = new Proxy(obj, {
  3. get(target, key, receiver) {
  4. console.log('获取值')
  5. return Reflect.get(target, key, receiver)
  6. },
  7. set(target, key, value, receiver) {
  8. console.log('设置值')
  9. return Reflect.set(target, key, value, receiver)
  10. }
  11. })
  12. nobj.a = '代理'
  13. console.log(obj)
  14. // 结果
  15. 设置值
  16. {a: "代理"}

上面的get,setProxy支持的拦截方法,而Proxy 支持的拦截操作有13种之多,具体可以参照ES6-Proxy文档,前面提到,Object.definePropertygettersetter方法并不适合监听拦截数组的变化,那么新引入的Proxy又能否做到呢?我们看下面的例子。

  1. var arr = [1, 2, 3]
  2. let obj = new Proxy(arr, {
  3. get: function (target, key, receiver) {
  4. // console.log("获取数组元素" + key);
  5. return Reflect.get(target, key, receiver);
  6. },
  7. set: function (target, key, receiver) {
  8. console.log('设置数组');
  9. return Reflect.set(target, key, receiver);
  10. }
  11. })
  12. // 1. 改变已存在索引的数据
  13. obj[2] = 3
  14. // result: 设置数组
  15. // 2. push,unshift添加数据
  16. obj.push(4)
  17. // result: 设置数组 * 2 (索引和length属性都会触发setter)
  18. // // 3. 直接通过索引添加数组
  19. obj[5] = 5
  20. // result: 设置数组 * 2
  21. // // 4. 删除数组元素
  22. obj.splice(1, 1)

显然Proxy完美的解决了数组的监听检测问题,针对数组添加数据,删除数据的不同方法,代理都能很好的拦截处理。另外Proxy也很好的解决了深层次嵌套对象的问题,具体读者可以自行举例分析。