vue响应式原理之defineProperty和Proxy

265 阅读5分钟

其实在学习vue时候一直对响应式比较困惑,一直想总结一下,今天就整理记录下来,当然了这篇文章参考了好多同学的,在文章后面有标注,各位看官如果对这个文章感兴趣的话,可以看一下,谢谢!

Object.defineProperty()

介绍vue响应式原理,首先应该介绍一下Object.defineProperty()这个方法,vue的响应式原理基于此,该方法在对象上定义了一个新属性,或者修改对象的属性,并且返回了这个对象。虽然我们可以手动添加属性和值,但使用Object.defineProperty()方式,可以进行更多的配置。函数的参数如下所示:

Object.defineProperty(obj,prop,descriptor)

参数:

  • obj:指的是定义属性的对象
  • prop:指的是定义或者修改属性的名称
  • descriptor:指的是将定义或修改属性的描述符

返回值:

  • 被传递给函数的对象

举个例子

Object.defineProperty(obj,'num',{
    value:1,
    writable:true,
    enumerable:true,
    configurable:true
})
//这个对象拥有属性num,属性值为1

descriptor是一个对象形式的,表示的是属性描述符,有两种形式:数据描述符和存取描述符。

两种描述符有两个共同的的键值:

  • configurable:表示的是该属性的描述符是否可以被改变,属性是否可以被删除,当键值为true时,属性描述值才能被改变,同时该属性也能从对象上被删除,默认值为false
  • enumerable:表示该属性是否是枚举属性,当键值为true时,该属性才会出现在对象的枚举属性中,默认为false

数据描述符:指的是一个具有值的属性,该值可以是可写的,也可以是不可写的,有两种可选键值:

  • value:属性对应的值,默认值为undefined
  • writable:属性值是否可以被改写,当writabletrue时,属性值才能被运算符改变,默认为false

存取描述符:指的是getter函数和setter函数所描述的属性

  • get:属性提供getter方法,当访问该属性时,会调用该函数,该方法返回值当作属性值,如果没有getter方法,则为undefined,默认为undefined
  • set:属性提供setter方法,当属性值被修改时,会调用这个函数,该方法接受一个参数,也就是被赋予的新值,如果没有setter方法,则为undefined,默认为undefined

默认值可以进行汇总:

  • 拥有布尔值的键 configurable、enumerable 和 writable 的默认值都是 false
  • 属性值和函数的键 value、get 和 set 字段的默认值为 undefined

属性描述值必须是数据描述值或者存取描述值两种形式之一,不能同时是两者,两者同时存在的话,会报异常;如果只有`configurable`和`enumerable`两个描述值,则会认为是数据描述符

以下两种是正确用法:

Object.defineProperty({},'num',{
    value:1,
    writable:true,
    enumerable:true,
    configurable:true
})
var value=1;
Object.defineProperty({},'num',{
    get:function(){
        return value;
    },
    set:function(newValue){
        value=newValue;
    },
    enumerable:true,
    configurable:true
})

属性描述值都是非必须的,但是descriptor这个字段是必须的,可以这样写

var obj=Object.defineProperty({},'num',{});
console.log(obj.num) //undefined

响应式其实是和存取描述符里面的get和set相关,看下下面的例子

var obj={},value=null;
Object.defineProperty(obj,'num',{
    get:function(){
        console.log('执行了get操作');
        return value;
    },
    set:function(newValue){
        console.log('执行了set操作');
        value=newValue
    }
})
obj.num=1;  //执行了set操作
obj.num     //执行了get操作

上面例子当我们取得属性值,或者进行赋值操作的时候,就可以在是、get或者set函数执行其他的操作,那我们是不是可以在这个时间段进行DOM的重新渲染,也就是实现了数据响应。接下来我们再讲一下另一个知识点。

观察者模式

观察者模式是一种一对多对象之间的关系。定义对象间一对多的依赖关系,比如当一个对象(目标对象)的状态发生改变时,所以依赖它的对象(观察者)的都会得到通知并自动更新,那么究竟是如何实现的呢?我们下面看个例子

class Subject{
    constructor(){
    this.observers=[]
    }
    add(observer){
        this.observers.push(observer)
    }
    notify(){
        this.observers.forEach(observer=>{
            observer.update();
        })
    }
}
class Observer{
    constructor(name){
        this.name=name;
    }
    update(){
        console.log(`通知更新数据,我是${this.name}`)
    }
}
let subject=new Subject();
let obs1=new Observer('观测点1');
let obs2=new Observer('观测点2');
subject.add(obj1);
subject.add(obj2);
subject.notify();//通知更新数据,我是观测点1
                 //通知更新数据,我是观测点2

上面的目标对象的类Subject拥有add方法可以添加观察者对象,notify方法可以通知观察者对象;观察者对象的类(Observer)对象拥有状态变更的方法;整个过程就是,目标对象的类Subject生成一个目标对象实例subject,目标对象实例使用add()方法可以将生成的观察者对象实例observer添加到内部依赖observers里面去,当目标对象实例状态发生变更的时候,目标对象实例调用notify方法,直接通知添加到内部依赖的观察者对象实例。所有添加到内部依赖的观察者对象实例调用自己内部的update()方法进行数据更新,这就是整个过程。其实本质上来说就是一对多的对象关系,当一个对象的状态发生改变,所有依赖这个状态的对象都要发生变化。

发布订阅模式

我经常听到发布订阅模式这个名词,可一直和观察者模式傻傻分不清,现在就介绍以下,直接上代码

let Pubsub={
    list:{},
    subscribe:function(key,fn){
        if(!this.list[key]){
            this.list[key]=[]
        }
        this.list[key].psuh(fn);
    },
    publish:function(key,...args){
        for(let fn of this.list[key]){
            fn.call(this,...args)
        }
    },
    unSubscribe:function(key,fn){
        let fnList=this.list[key];
        if(!fnList) return false;
        if(!fn){
            fnList&&(fnList.length=0)
        }else{
            fnList.forEach((item,index)=>{
                if(item==fn)
                fnList.splice(index,1)
            })
        }
    }
}
Pubsub.subscribe('onwork',time=>{console.log(`王华上班了${time}`)})
Pubsub.subscribe('onwork',time=>{console.log(`小明上班了${time}`)})
Pubsub.publish('onwork','18:00')

在这个模式里,订阅者subscribe订阅消息,消息存储在Pubsub中,当需要发布消息的时候,发布者publish发布某个消息,订阅这个消息的订阅者都会收到这个消息。那么这个和观察者模式有什么区别呢?

观察者模式和发布订阅模式异同

很多人都觉得他们是一样的,其实还是有一些区别的,观察者模式是为了实现松耦合,那什么是松耦合呢,借用《Head First设计模式》里面的气象站的例子,每当气象测量数据有更新,changed()方法就会被调用,于是我们可以在changed()方法里面,更新气象仪器上的数据,比如温度、气压。但是这样写有一个问题,就是因为业务需要如果我们想要在changed()方法被调用的时候,更新更多的信息,比如湿度,那就要修改changed()代码,因为业务需要我们还需要更新紫外线强度,又要再次修改changed()代码,这就是紧耦合的坏处。

怎么解决呢?可以使用观察者模式,实现松耦合,

在观察者模式中,changed()方法所在的实例对象,就是目标对象Subject,changed()方法相当于目标对象的notify()方法,目标对象Subject需要维护观察者对象实例observer,将观察者对象添加到observers整个集合,当我们需要添加新的维护内容,新建观察者对象,添加observers中去就可以了,等到数据更新的时候,目标对象会通知observers集合中的每一个观察者对象。

大家第一眼看见发布订阅模式,肯定都觉得发布者publish就是目标对象Subject而订阅者subscribe就是观察者observer。当状态变化时,发布者发布消息直接通知订阅者,其实不是这样的,发布订阅模式中发布者并不会直接通知订阅者,发布者和订阅者是通过事件中心交流的,并不会直接联系,订阅者需要订阅的时候直接给事件中心说,我要订阅什么的消息,发布者需要发布消息的时候直接给事件中心说,我需要发布什么的消息,也就是说发布订阅模式是直接解耦的。

他们主要的区别在于:观察者模式,主要有两个概念,目标对象和观察者对象,目标对象将所有的观察者对象存储在一个聚集里,当状态发生变更时,通知这个聚集里所有的观察者对象。发布订阅模式比观察者模式多了一个事件通道,订阅着发布者不是直接关联的,发布订阅者信息交流是通过事件中心传递的。观察者模式是松耦合的,发布订阅模式是解耦的,从实际场景上来说,发布订阅模式有更高的适用性。

简单实现一个响应式

<div id="app">
</div>
  var render = function(template, data) {
      const reg = /\{\{(\w+)\}\}/; 
      if (reg.test(template)) {
          const key = reg.exec(template)[1]
          template = template.replace(reg, data[key]);
          return  render(template, data) 
      }
      return template
  }
  const obj={
      name:'张三',
      age:'18',
      sex:'男'
  }
  class Dep{
      constructor(){
          this.subs=[]
      }
      addSub(watch){
          this.subs.push(watch)
      }
      notify(){
          this.subs.foeEach(item=>{
              item.update();
          })
      }
  }
  class Watch{
      constructor(name){
          this.name=name
      }
      update(){
          console.log(this.name+'更新视图');
          document.getElementById('app').innerHTML=render(template,obj);
      }
  }
  Object.keys(obj).forEach(key=>{
      let dep=new Dep();
      let value=obj[key];
      Object.defineProperty(obj,key,{
          set:function(newValue){
            console.log(`数据${key}更新了`)
            value=newValue
            dep.notify()
          }
          get:function(){
            console.log(`数据${key}加入了响应式系统`)
            let w=new Watch(value);
            dep.addsub(w);
            return value;
          }
      })
  })
  obj.message = "没添加响应的数据";
  var template = '我是{{name}}, {{name}}  年龄 {{age}},性别 {{sex}} 。 {{message}} ';
  document.getElementById('app').innerHTML = render(template,obj);

上面这个例子首先遍历obj的属性,然后新建一个发布者实例,,使用object.defineProperty()方法,将这些属性转化为settergetter,当我们在网页中渲染模版的时候,比如渲染obj.name,会获取obj的属性值,触发objgetter,在getter函数里会新建一个订阅者实例,并且把这个订阅者实例添加到属性相对应的发布者列表上去,如果页面的其他地方也有渲染obj.name,那么就还是要新建一个订阅者实例,并且把这个订阅者实例添加到属性相对应的发布者实例列表上去。(也就是每个属性都有一个相对应的发布者实例,页面上这个属性的引用都会生成一个订阅者者实例,这个订阅者者实例被添加到相对应的发布者实例中去)。当我们更新obj的数据时,会发生什么呢?会调用obj属性的setter方法,setter方法会调用这个属性目标对象实例的notify,发布者实例会通知每一个依赖这个属性状态的订阅者实例,也就是页面中绑定的这个属性值发生变化。这样就完成了响应式数据变化。

下面我引入vue官方对自己响应式原理的解释

vue官网:当你把一个普通的 JavaScript 对象传入Vue 实例作为 data 选项,Vue 将遍历此对象所有的 property,并使用 Object.defineProperty把这些 property 全部转为 getter/setter。这些 getter/setter 对用户来说是不可见的,但是在内部它们让 Vue 能够追踪依赖,在 property 被访问和修改时通知变更。每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据 property 记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。

看完之后是不是和我们写的很像呀,我用简短的语言再描述一下我们的响应式,首先遍历obj的属性,每个属性生成一个依赖收集器,当我们在页面进行数据绑定的时候,触发属性的getter方法,并且把这个生成订阅者者对象收进依赖收集器中,添加响应式的依赖;当我们重新给对象赋值,触发setting时,通知依赖收集器中所有的订阅者状态发生变更,进行DOM的重新渲染。那这个就是vue的响应式原理吗?其实vue的响应式更复杂一些,我们只是简单的模拟了一下,还存在很多问题,比如data数据数据嵌套复杂数据怎么办,比如data的数据没有用到页面渲染时,当我们对其进行更改,页面是否会重新更新等等。带着这些疑问,我们再写一个相对完整的

<div id="app">
    <p>{{message}}</p>
    <span>{{message}}</span>
</div>
data(){
    return{
        message:'我是数据',
        text:'我是额外的数据'
    }
}

我们先来想一下,怎么才能做到响应式,我们更改data里面的message,页面上绑定的message都发生变化这是我们的直观感受,实际工作可远不止这些,我们会想一下我们刚写的响应式,当我们改变obj.text的值,会发生什么变化呢?会触发setter,并且将重新渲染页面,可是obj.text这个值根本就没有挂载到页面上,显然是不可取的。接下来我们就实现只有挂载到页面的数据更新才会引起页面重新渲染,想要完成这个工作首先是要完成下面几个步骤

  • 监听到data哪个数据发生变化
  • 收集页面视图都有哪些数据是响应式数据
  • 数据变化时,自动通知需要更新的视图进行更新

用更加专业的话,这三个步骤是

  • 数据劫持/数据代理
  • 依赖收集
  • 发布订阅模式

我们都知道,vue能够实现当一个数据变更的时候,视图跟着进行刷新,用到这个数据的地方会同步进行更新,而且这个数据必须是在有被依赖的情况下,视图和其他用到数据的地方才会变更。而vue知道一个数据是否使用的东西,这个机制叫做依赖收集。

那这个该怎么实现呢?要想实现vue响应式功能还需要补充几个概念。

  • Observer:数据的观察者,让数据对象的读写操作处于自己的监管之下,当初始化实例的时候,会递归 遍历data,运用Object.defineProperty()函数劫持数据的settergetter。在数据变动的时候发布消息给订阅者。

  • Dep:数据更新的发布者,含有订阅者的列表类,可以增加或者删除订阅者,可以向订阅者发送消息,当getter数据的时候,收集订阅者;当setter数据的时候,发布更新数据,通知Watcher

  • Watcher:数据更新的订阅者,它在初始化时可以接受getter,callback两个函数作为参数。getter用来计算Watcher对象的值。当Watcher被触发时,会重新通过getter计算当前Watcher的值,如果值改变,则会执行callback

function observe(obj) {
  // 判断传进来数据类型
  if (!obj || typeof obj !== 'object') {
    return
  }
  //遍历对象的每一个属性
  Object.keys(obj).forEach(key => {
    //对每一个属性进行劫持
    defineReactive(obj, key, obj[key])
  })
}
function defineReactive(obj, key, val) {
  //如果val是一个复杂类型,还需要进行深度劫持
  observe(val)
  //实例化一个Dep,这个Dep存在于下面的get和set函数的作用域里,用来收集订阅数据更新的Wather,这里的Dep与参数的key相对应,每个key值都有一个订阅者列表。
  var dep=new Dep()
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      console.log('get value')
      if(Dep.target){
          //Watcher对象存在全局的Dep.target中
          dep.addSub(Dep.target)
      }
      return val
    },
    set: function reactiveSetter(newVal) {
      console.log('change value')
      val = newVal
      dep.notify()
    }
  })
}
// 发布者,利用数据的setter,调用dep.addSub()收集依赖,修改数据的时候触发dep.notify订阅者。
class Dep {
  constructor() {
    this.subs = []
  }
  // 添加依赖
  addSub(sub) {
    this.subs.push(sub)
  }
  // 更新
  notify() {
    this.subs.forEach(sub => {
        //只有之前addSub中的函数才会触发
      sub.update()
    })
  }
}
// 全局属性,通过该属性配置 Watcher
Dep.target = null
//订阅者,页面渲染数据的时候新建Watcher实例,调用数据的getter,将自己的实例被dep收集依赖
class Watcher {
  constructor(obj, key, cb) {
    // 将 Dep.target 指向自己
    // 然后触发属性的 getter 添加监听
    // 最后将 Dep.target 置空
    Dep.target = this
    this.cb = cb
    this.obj = obj
    this.key = key
    //触发数据的getter,此时的Dep.target指向新建的watcher实例
    this.value = obj[key]
    Dep.target = null
  }
  update() {
    // 获得新值
    this.value = this.obj[this.key]
    // 调用 update 方法更新 Dom
    this.cb(this.value)
  }
}
//模拟vue里面的data数据
var data = { name: 'yck' }
//劫持data数据
observe(data)
//模拟数据更新后的回调函数
function update(value) {
  document.querySelector('div').innerText = value
}
// 模拟解析到模版{{name}}触发的操作
new Watcher(data, 'name', update)
//模拟更新数据
data.name = 'yyy' 

我们来分析一下整个过程,

  • 首先使用observe(data)劫持我们的数据,为每一个属性生成一个依赖收集器dep,当setter数据时会触发更新,当getter数据时会触发依赖收集,这个时候会判断Dep.target是否有值,这也是视图上的数据才会被收集依赖的关键。
  • 模拟在页面上挂载数据,首先生成一个Watcher实例,将自己赋值给全局变量Dep.target,当执行this.value = obj[key]时,触发数据的getter,此时Dep.target是有值的,所以dep收集这个Watcher实例。
  • 当更新数据的时候,调用收集器中dep.notify进行数据更新。

这个也是简单的实现了,还有很多问题,比如defineProperty对数组的方法不是匹配的很好,更新数组数据还不能很好的访问等,这些问题留到下次解决了。下面附上源码。为了方便看,我改成了es6格式。

class Observer{
    constructor(value){
        this.value=value;
        this.dep=new Dep();
        this.vmCount=0;
        def(value,'__ob__',this);
        if(Array.isArray(value)){
            var augment=hasProto?protoAugment:copyAugment;
            augment(value,arraymethods,arrayKeys);
            this.observeArray(value);
        }else{
            this.walk(value)
        }
    }
    walk(obj){
        var keys=Object.keys(obj);
        for(var i=0;i<keys.length;i++){
            defineReactive?1(obj,keys[i],obj[keys[i]]);
        }
    }
    observeArray(items){
        for(var i=0,l=items.length;i<l;i++){
            observe(items[i]);
        }
    }
}
function defineReactive?1(obj,key,val,customSetter){
    var dep=new Dep();
    var property=Object.getOwnPropertyDescriptor(obj,key);
    if(property&&property.configurable===false){
        return
    }
    var getter=property&&property.get;
    var setter=property&&property.set;
    var childOb=observe(val);
    Object.defineProperty(obj,key,{
        enumerable:true,
        configurable:true,
        get:function rectiveGetter(){
            var value=getter?getter.call(obj):val;
            if(Dep.target){
                dep.depend();
                if(childOb){
                    childOb.dep.depend();
                }
                if(Array.isArray(value)){
                    dependArray(value)
                }
            }
            return value
        },
        set:function reactiveSetter(newVal){
            var value=getter?getter.call(obj):val;
            if(newVal===value||(newVal!==newVal&&value!==value)){
                return
            }
            if("development"!=='production'&&customSetter){
                customSetter();
            }
            if(setter){
                setter.call(obj,newVal);
            }else{
                val=newVal;
            }
            childOb=observe(newVal);
            dep.notify();
        }
    })
}
function dependArray(value){
    for(var e=(void 0),i=0,l=value.length;i<l;i++){
        e=value[i];//在调用这个函数的时候,数组已经被observe过了,并且会递归observe,所以会存在__ob__属性,这个时候需要调用dep添加依赖。
        e&&e.__ob__&&e.__ob__.dep.depend();
        if(Array.isArray(e)){
            dependArray(e);
        }
    }
}
var uid$1=0;
class Dep{
    constructor(){
        this.id=uid$1++;
        this.subs=[];
    }
    addSub(){
        this.subs.push(sub);
    }
    depend(){
        if(Dep.target){
            Dep.target.adddep(this)
        }
    }
    notify(){
        var subs=this.subs.slice();
        for(var i=0,l=subs.length;i<l;i++){
            subs[i].update();
        }
    }
    removeSub(){
        this.subs=this.subs.slice();
        for(var i=0,l=subs.length;i<l;i++){
            subs[i].update();
        }
    }
}
Dep.target=null;
class Watcher{
    constructor(vm,expOrFn,cb,oprions){
        this.vm=vm;
        vm._watchers.push(this);
        if(options){
            this.deep=!!
            this.deep = !!options.deep;
            this.user = !!options.user;
            this.lazy = !!options.lazy;
            this.sync = !!options.sync;
        } else {
            this.deep = this.user = this.lazy = this.sync = false;
        }
        this.cb = cb;
        this.id = ++uid$2; 
        this.active = true;
        this.dirty = this.lazy; 
        this.deps = [];
        this.newDeps = [];
        this.depIds = new _Set();
        this.newDepIds = new _Set();
        this.expression = expOrFn.toString();
        if (typeof expOrFn === 'function') {
            this.getter = expOrFn;
        } else {
            this.getter = parsePath(expOrFn);
            if (!this.getter) {
                this.getter = function () {};
            "development" !== 'production' && warn("Failed watching path: \"" + expOrFn + "\" " +'Watcher only accepts simple dot-delimited paths. ' +'For full control, use a function instead.',vm );
                }
        }
        this.value = this.lazy? undefined: this.get();
    }
    get(){
        pushTarget(this);
        var value;
        var vm=this.vm;
        try{
            value=this.getter.call(vm,vm)
        }catch(e){
            if(this.user){
                handleError(e,vm,("getter for watcher\""+(this.expression)+"\""))
            }else{
                throw e
            }
        }finally{
            if(this.deep){
                traverse(value);
            }
            popTarget();
            this.cleanupDeps();
        }
        return value;
    }
   addDep(){
        var id = dep.id;
        if (!this.newDepIds.has(id)) {
            this.newDepIds.add(id);
            this.newDeps.push(dep);
            if (!this.depIds.has(id)) {
                dep.addSub(this);
            }
        }
    }
    cleanupDeps(){
        var this$1 = this;
        var i = this.deps.length;
        while (i--) {
            var dep = this$1.deps[i];
            if (!this$1.newDepIds.has(dep.id)) {
                dep.removeSub(this$1);
            }
        }
        var tmp = this.depIds;
        this.depIds = this.newDepIds;
        this.newDepIds = tmp;
        this.newDepIds.clear();
        tmp = this.deps;
        this.deps = this.newDeps;
        this.newDeps = tmp;
        this.newDeps.length = 0;
    }
    update(){
        if (this.lazy) {
            this.dirty = true;
        } else if (this.sync) {
            this.run();
        } else {
            queueWatcher(this);
        }
    }
    run(){
        if (this.active) {
            var value = this.get();
            if (value !== this.value ||isObject(value) ||this.deep) {
                var oldValue = this.value;
                this.value = value;
                if (this.user) {
                    try {
                        this.cb.call(this.vm, value, oldValue);
                    } catch (e) {
                        handleError(e, this.vm, ("callback for watcher \"" + (this.expression) + "\""));
                    }
                } else {
                    this.cb.call(this.vm, value, oldValue);
                }
            }
        }
    }
}
Dep.target = null;
var targetStack = [];
function pushTarget (_target) {
    if (Dep.target) { targetStack.push(Dep.target); }
    Dep.target = _target;
}
function popTarget () {
    Dep.target = targetStack.pop();
}
var bailRE = /[^\w.$]/;
function parsePath (path) {
    if (bailRE.test(path)) {
        return
    }
    var segments = path.split('.');
    return function (obj) {
        for (var i = 0; i < segments.length; i++) {
            if (!obj) { return }
            obj = obj[segments[i]];
        }
    return obj
  }
}

其实还有一个问题,那我们后面添加的objmessage属性能不能被响应呢,答案是不能的,这是因为当我们对obj属性添加响应式依赖的时候,message还没有被添加,这也是为什么vue初始化实例前必须声明所有的根级响应式属性。

那为什么vue从3.0将使用Proxy代替definProperty呢?

这是因为通过下标方式修改数组数据或者给新对象新增属性不能触发组件的重新渲染,Object.defineProperty()是对已经遍历的每一个对象属性添加的响应式依赖,而不会对我们后来添加的属性添加响应式依赖。接下来我们也将学习Proxy

Proxy

Proxy对象用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等)。使用Object.defineProperty()只可以定义属性的读取和设置行为,但无法定义其他的行为。es6提供了Proxy,可以定义更多的行为,Proxy这个词的意思是代理,在对目标对象的操作进行了拦截,对外部的操作进行过滤和改写,修改某些操作的默认行为,我们不直接操作对象本身,而是直接操作代理对象。

var proxy=new Proxy(target,handler);

这是这个对象的用法,target是我们代理的对象,handler是一个对象定制拦截行为,,new Proxy生成一个Proxy实例。

var proxy=new Proxy({},{
    get:function(obj,prop){
        console.log('设置了get操作');
        return obj[prop];
    },
    set:function(obj,prop,value){
        console.log('设置了set操作');
        obj[prop]=value;
    }
});
proxy.time=35 //设置了set操作
proxy.time    //设置了get操作

这个是不是和我们Object.defineProperty()很相像呢,不一样的一点是Proxy对整个对象进行拦截,当对对象属性取值或者赋值的时候,就会触发的属性的setgetObject.defineProperty()是对对象属性进行操作,当对操作过的属性进行取值或者赋值的时候,才会触发属性的setget;换句话说Proxy关注的是对象,Object.defineProperty()关注的是对象的属性。 当然了Proxy做的可不止这些,除了getset以外,proxy可以拦截多达13种操作,比如has(target,propKey),可以拦截propKey in proxy的操作,返回一个布尔值,其他情况就不再赘述了,大家可以自行查阅MDN相关资料。下面将会探究Proxy如何实现响应式。

function reactive(target){
    const handler ={
        get(target,key,receiver){
            console.log(`${key}执行了get操作,加入了响应`)
            //可以将数据添加到响应式依赖里
            return Reflect.get(target,key,receiver);
        }
        set(target,key,value,receiver){
            console.log(`${key}执行了set操作,更新了数据`)
            //可以执行数据渲染的操作
            return Reflect.set(target,key,value,receiver);
        }
    }
    let observed=new Proxy(target,handler);
    return observed;
}
let obj={
    name:'song'
}
let proxyObj=reactive(obj);
proxyObj.name //name执行了get操作,加入了响应
proxyObj.name='wang' //name执行了set操作,更新了数据
proxyObj.age=13 //age执行了set操作,更新了数据

如上面所示,当我们添加一个不存在的属性时,也会进入到响应式中,这也是和defineProperty原来不同的地方。

扩展一个Reflect对象知识,Reflect对象与Proxy对象一样,都是ES6为了操作对象提供的API,Reflect设计的目的有以下几个:

  • Object对象上一些明显属于语言内部的方法,比如Object.defineProperty()放到Reflect对象上面去,现阶段,这些方法同时放在ObjectReflect上,未来只会部署在Reflect对象上。
  • Object操作都变成函数行为,某些Object行为都是命令式的,name in objdelete obj[name],而Reflect.has(obj,name)Reflect.deleteProperty(obj,name)让他们都变成了函数行为。
  • Reflect对象的方法与Proxy对象的方法一一对应,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。这就让Proxy对象可以方便地调用对应的Reflect方法,完成默认行为,作为修改行为的基础。也就是说,不管Proxy怎么修改默认行为,你总可以在Reflect上获取默认行为。
  • 修改某些Object方法返回的结果,让其变得更加合理,比如Object.defineProperty()无法定义属性时,会抛出一个错误,而Reflect.defineProperty()会返回false
//以前写法
try{
    Object.defineProperty(obj,prop,descriptor);
    //success
}catch(e){
    //failure
}
//现在写法
if(Reflect.defineproperty(obj,prop,descriptor)){
    //success
}else{
    //failure
}

这种响应存在一个问题,看下面

let array=[1,2,3];
let proxyArray=reactive(array);
proxyArray.push(4) 

这种情况会发生两个更新数据,一次是数组下标更新触发,一次是length更新触发,但是length这个更新是我们不需要的 ,因此可以对这个函数进行更新

function reactive(target){
    const handler ={
        get(target,key,receiver){
            console.log(`${key}执行了get操作,加入了响应`)
            //可以将数据添加到响应式依赖里
            return Reflect.get(target,key,receiver);
        }
        set(target,key,value,receiver){
            if(!target.hasOwnProrerty(key)){
                console.log(`${key}执行了set操作,更新了数据`)
                //可以执行数据渲染的操作
            }
            return Reflect.set(target,key,value,receiver);
        }
    }
    let observed=new Proxy(target,handler);
    return observed;
}

还有一个情况,如果obj对象里面有个数组呢,数组会是响应式的吗?

let obj={
    name:'test name',
    array1:[1,2,3]
}
let proxyObj=reactive(obj);
proxyObj.name //name执行了get操作,加入了响应
proxyObj.name='wang' //name执行了set操作,更新了数据
proxyObj.array1 //array1执行了get操作,加入了响应
proxyObj.array1.push(4)//array1执行了get操作,加入了响应

如上所示,array1数组也加入了响应,但是向array1添加数据的时候,不会触发set,也就没办法重新进行渲染工作,所以应该对其是否为对象做一个判断,是对象的话,还要继续继续递归,

function reactive(target){
    const handler ={
        get(target,key,receiver){
            console.log(`${key}执行了get操作,加入了响应`)
            //可以将数据添加到响应式依赖里
            const proxyTarget=Reflect.get(target,key,receiver)
            if(typeof target[key]==='object' && target[key]!==null){
                return reactive(proxyTarget)
            }
            return proxyTarget;
        },
        set(target,key,value,receiver){
            if(!target.hasOwnProperty(key)){
                console.log(`${key}执行了set操作,更新了数据`)
                //可以执行数据渲染的操作
            }
            return Reflect.set(target,key,value,receiver);
        }
    }
    let observed=new Proxy(target,handler);
    return observed;
}

参考:

Object.defineProperty()

Proxy 手摸手从0实现简版Vue --- (依赖收集)

ES6 系列之 defineProperty 与 proxy

简单理解Vue响应式原理

使用 Proxy 实现 Vue.js 3 中的响应式思想