Vue3.0之proxy代替object.definProperty()

在最新的Vue 3.x,一个很重要的改变就是将使用 ES6Proxy 作为其观察者机制,取代之前使用的Object.defineProperty。对于Object.defineProperty大家应该在学习Vue中的响应式数据时深有体会,它可以 重写属性的 getset 方法,从而完成监听数据的改变。

1. Vue 2.x中的 Object.defineProperty实现响应式数据

简单的用input实现一个v-model




    
    vue双向绑定实现







下面我们看一下在vue中是怎么实现的:

  • Observer:数据的观察者,让数据对象的读写操作都处于自己的监管之下。当初始化实例的时候,会递归遍历data,用Object.defineProperty来拦截数据。

  • Dep:数据更新的发布者,get数据的时候,收集订阅者(dep.addSub()),触发Watcher的依赖收集(this.subs.push(sub));set数据时通知Watcher(dep.notify()),发布更新(update()) 。

  • Watcher:数据更新的订阅者,订阅的数据改变时执行相应的回调函数(更新视图或表达式的值)。

大家可以参考我在GitHub上的vueMVVM的具体实现:https://github.com/GitHubzl0212/MVVM。

但是在vue3+中要用Proxy取代自然有作者的道理。简单来说Object.defineProperty有以下几个缺陷:

⑴ 无法检测数组的变化

Object.defineProperty无法监控到数组下标的变化,导致直接通过数组的下标给数组设置值,不能实时响应。 而且使用这些方法(push, pop, shift, unshift,splice, sort, reverse…)是不能触发set的,Vue中能监听是因为对这些方法进行了重写;

var a = {},
  bValue = 1;
Object.defineProperty(a,"b",{
  set: function(value){
    bValue = value;
    console.log("setted");
  },
  get: function(){
    return bValue;
  }
});

a.b = []; //setted
a.b = [1,2,3]; //setted
a.b[1] = 10; //无输出
a.b.push(4); //无输出
a.b.length = 5; //无输出

a.b被设置为数组后,只要不是重新赋值一个新的数组对象,任何对数组内部的修改都不会触发setter方法的执行。所以要想实现实现数组的双向绑定,则必须通过Arr = newArr;这样的语句实现。同样常见的数组方法也不会触发,在框架中对这些方法进行了重写才能实现效果。

⑵ 只能监听属性,而不是监听对象本身,需要对对象的每个属性进行遍历。对于原本不在对象中的属性难以监听。在Vue 2.x里,是通过 “callback + 遍历 data 对象” 来实现对数据的监控的,如果属性值也是对象那么需要深度遍历,显然如果能劫持一个完整的对象是才是更好的选择,而Proxy就显示了这方面的优势。

⑶ 当对象增删的时候,是监控不到的。比如:data = {a:"a"},这个时候如果我们设置data.test = "test",这个时候是监控不到的。因为在observe data的时候,会遍历已有的每个属性(比如a),添加getter/setter,而后面设置的test属性并没有机会设置getter/setter,所以检测不到变化。同样的,删除对象属性的时候,getter/setter会跟着属性一起被删除掉,拦截不到变化。

2. ES6中的Proxy

Proxy可以理解为“代理”而不是“拦截”的意思,Proxy可以拦截js引擎内部目标的底层对象操作,这些底层对象操作被拦截后会触发响应特定操作的陷阱函数,每个代理陷阱对应一个命名和参数一致的Reflect方法。ES6中扩展了13个代理陷阱:
Vue3.0之proxy代替object.definProperty()_第1张图片
⑴ 使用set陷阱验证属性

set陷阱函数接受四个参数:

  • trapTarget 用于接收属性(代理的目标)的对象
  • key 要写入的属性键
  • value 被写入属性的值
  • receiver 操作发生的对象(通常是代理)

下面实现一个属性值时数字的对象,对象中每新增一个属性都要加以验证,如果不是数字就抛出错误。

let target = {
  name: "target"
};

let proxy = new Proxy(target, {
  set(trapTarget, key, value, receiver) {
    //排除已有的属性
    if(!trapTarget.hasOwnProperty(key)) {
      if(isNaN(value)) {
        throw new TypeError("属性必须是数字");
      }
    }
    //添加属性
    return Reflect.set(trapTarget, key, value, receiver);
  }
});

//添加新属性
proxy.count = 1;
console.log(proxy.count);  //1
console.log(target.count);  //1

//由于目标已有name属性,因而可以给他赋值
proxy.name = "proxy";
console.log(proxy.name); //proxy
console.log(target.name);  //proxy

//给不存在的属性赋值会发生错误
proxy.newname = "apple";  //属性必须是数字

⑵ 使用get陷阱验证对象结构

get陷阱函数接受三个参数:

  • trapTarget 被读取的属性的源对象(代理的目标)
  • key 要读取的属性键
  • receiver 操作发生的对象(通常是代理)

在大多数语言中,如果target没有name属性,尝试读取target.name会抛出一个错误。但是js中却用undefined来代替target.name的属性的值。而代理可以通过检查对象结构来帮助我们回避这个问题。

let proxy = new Proxy({}, {
  get(trapTarget, key, receiver) {
    if(!(key in receiver)) {
      throw new TypeError("属性" + key + "不存在");
    }
    return Reflect.get(trapTarget, key, receiver)
  }
});

//添加一个属性
proxy.name = "proxy";
console.log(proxy.name); //proxy

//如果属性不存在,则抛出错误
console.log(proxy.nam);  //TypeError: 属性nam不存在

⑶ 使用has陷阱隐藏已有属性

可以用in操作符来检测给定对象中是否含有某个属性,如果自有属性或原型属性匹配这个名称或Symbol就返回true,例如:

let target = {
  value: 42
};

console.log("value" in target);  //true, 自有属性
console.log("toString" in target);  //true, 继承自Object的原型属性

在代理中使用has陷阱可以拦截这些in操作并会返回一个不同的值。

has陷阱函数接受两个参数:

  • trapTarget 读取属性的对象(代理的目标)
  • key 要检查的属性键(字符串或Symbol)

例如,可以同时使用has陷阱和Reflect.has()改变一部分属性被in检测时的行为

let target = {
  name: "target",
  value: 42
};

let proxy = new Proxy(target, {
  has(traptarget, key) {
    if(key==="value") {
      return false
    } else {
      return Reflect.has(traptarget, key)
    }
  }
});

console.log("value" in proxy);  //false
console.log("name" in proxy);   //true
console.log("toString" in proxy);  //true

代理中的has陷阱会检查key是否为“value”,如果是的话返回false,若不是则调用Reflect.has()方法返回默认行为。

参考:
https://segmentfault.com/a/1190000006599500
https://blog.csdn.net/ijarvis/article/details/80485972
《深入理解ES6》

你可能感兴趣的:(vue基础学习,JavaScript,proxy,defineproperty,双向数据绑定)