前言

        现在国内的两大框架:vue、react。对于这两个框架,相信大家多多少少都接触过,对于vue而言,有一个很重要的特点,那就是响应式。vue2的响应式采用的是ES5的Object.defineproperty对数据进行劫持。而vue3则是采用ES6的Proxy对数据进行劫持。想要了解vue3的响应式就必须了解Proxy。

一、Proxy的认识

       Proxy是ES6新增的,它是一个类,是用于帮助我们创建一个代理对象,如果我们需要监听对象的操作,那么我们可以通过Proxy先创建一个代理对象,之后对该对象的所有操作都通过代理来完成。他与Object.defineProperty最大的区别就在于Object.defineProperty直接监听对象的属性,proxy是监听整个代理对象。

二、Proxy的基本使用

创建一个对象

const obj = {
    name:'cj',
    age:18
}

再创建Proxy代理对象

const objProxy = new Proxy(obj,{})

 参数一:需要代理的对象  

参数二:捕获器,对代理对象的属性进行访问、赋值等操作的时候触发,与Object.defineproperty的存取描述符类似。如果为空对象,就只有set、get这两个默认捕获器,并且不会有过多的操作,get捕获器就直接返回访问属性的值,set捕获器就将新的值赋值给访问属性。

示例:

const objProxy = new Proxy(obj,{})

console.log(objProxy.name)   //'cj'
console.log(objProxy.age)    //18

objProxy.name = 'wx'
objProxy.age = 20

//对代理对象操作后,代理对象就会对原对象进行操作
console.log(obj.name)       //'wx'
console.log(obj.age)        //20

注意:Proxy只能代理对象(Object、Function、Array),非对象值无法进行代理。Proxy也只能够代理对象的基本操作,无法代理对象的复合操作。什么是基本操作?什么又是复合操作?

const obj = {
    name:'cj',
    bar(){
        console.log('bar')    
    }
}
const objProxy = new Proxy(obj,{})

function foo() { console.log('foo') }
const fooProxy = new Proxy(foo,{
    apply(){    //apply是对函数调用进行拦截
        //...
    }
})

objProxy.name;     //基本操作
fooProxy();    //基本操作

objProxy.bar()    //复合操作

上面访问代理对象的属性、调用函数就是基本操作。调用对象里的方法就是复合操作,它可以分为两步:第一步是访问对象的方法,第二步是调用该方法。

三、Proxy捕获器的使用

        Proxy捕获器用来对代理对象属性进行访问、赋值等操作时的一个捕获。与Object.defineproperty的存取描述符类似。下面我们就认识一下常用的四个捕获器的基本使用。

const objProxy = new Proxy(obj,{
    //get操作符
    get:function(target,key){
        console.log(`监听到访问${key}属性`,target)
        return target[key]    //返回访问属性的值
    },
    //set操作符
    set:function(target,key,newValue){
        console.log(`监听到给${key}属性设置值`,target)
        tarset[key] = newValue    //将属性最新值,赋值给代理对象属性
    }
    //has操作符
    has:function(target,key){
        console.log(`监听到使用in操作符${key}`,target)
        return key in target
    }
    //delete操作符
    deleteProperty:function(){
        console.log(`监听到使用delete操作符${key}`,target)
        delete target[key]    
    }
})

console.log(objProxy.name)
console.log(objProxy.age)

objProxy.name = 'wx'
objProxy.age = 20

//in操作符
console.log('name' in objProxy)  //true
//delete操作符
delete objProxy.name

Proxy的13种捕获器:

  • handler.get():属性读取操作的捕捉器。
  • handler.set():属性设置操作的捕捉器。
  • handler.has():in 操作符的捕捉器。
  • handler.deleteProperty():delete 操作符的捕捉器。
  • handler.getPrototypeOf():Object.getPrototypeOf 方法的捕捉器。
  • handler.setPrototypeOf():Object.setPrototypeOf 方法的捕捉器。
  • handler.isExtensible():Object.isExtensible 方法的捕捉器。
  • handler.preventExtensions():Object.preventExtensions 方法的捕捉器。
  • handler.getOwnPropertyDescriptor():Object.getOwnPropertyDescriptor 方法的捕捉器。
  • handler.defineProperty():Object.defineProperty 方法的捕捉器。
  • handler.ownKeys():Object.getOwnPropertyNames 方法和Object.getOwnPropertySymbols 方法的捕捉器。
  • handler.apply():函数调用操作的捕捉器。(函数也是一个对象,这里就对函数调用时进行监听)
  • handler.construct():new 操作符的捕捉器。(函数执行new操作符的时候进行监听)

总结:

  • Proxy除了set、get捕获器能监听对象属性,还有其它的操作符,一共有13种。
  • 使用Proxy创建的代理对象进行操作的好处是,可以不用直接通过Object.defineproperty去操作原对象,通过Object.defineproperty直接操作原对象就会将对象原本的数据属性描述符变成了访问属性描述符
  • Object.defineproperty没有类似于has、deleteProperty等这样的捕获器,只有存取属性描述符(set、get)

四、Reflect的认识

Reflect是一个对象,字面意思是"反射"。

Reflect有什么用呐?

它主要提供了很多操作JavaScript对象的方法,有点像Object中操作对象的方法; 比如Reflect.getPrototypeOf(target)类似于 Object.getPrototypeOf();

如果我们有Object可以做这些操作,那么为什么还需要有Reflect这样的新增对象呢?

这是因为在早期的ECMA规范中没有考虑到这种对 对象本身 的操作如何设计会更加规范,所以将这些API放到了Object上面;但是Object作为一个构造函数,这些操作实际上放到它身上并不合适;另外还包含一些类似于 in、delete操作符,让JS看起来是会有一些奇怪的;所以在ES6中新增了Reflect,让我们这些操作都集中到了Reflect对象上。

那么Object和Reflect对象之间的API关系,可以参考MDN文档:

MDN Web Docswww

五、 Reflect基本使用

我们对上面的代码做一下修改,使用Reflect代替一下

const objProxy = new Proxy(obj,{
    get:function(target,key){
        console.log(`监听到访问${key}属性`,target)
        return Reflect.get(target,key)    //改为Reflect.get
    },
    set:function(target,key,newValue){
        console.log(`监听到给${key}属性设置值`,target)
        Reflect.set(target,key,newValue)    //改为Reflect.set
    }
})

console.log(objProxy.name)

objProxy.name = 'wx'

用Reflect的好处是什么呐?

  1. 之前的方式是说到底还是在操作原对象,因为都是在用target、key等直接去操作,改用Reflect就真正意义上不直接操作原对象。
  2. 在有的时候Reflect会更加有用。比如:使用Object.freece(obj)将对象冻结后,之前的方式就无法判断出设置值到底是设置成功了还是失败了。而Reflect可以有返回值,代表是设置成功还是失败。

示例

const istrue = Reflect.set(target,key,newValue)
const result = istrue?'设置成功':"设置失败"

常见的Reflect方法

  • Reflect.getPrototypeOf(target):类似于 Object.getPrototypeOf()。
  • Reflect.setPrototypeOf(target, prototype):设置对象原型的函数. 返回一个 Boolean, 如果更新成功,则返回true。
  • Reflect.isExtensible(target):类似于 Object.isExtensible()
  • Reflect.preventExtensions(target):类似于 Object.preventExtensions()。返回一个Boolean。
  • Reflect.getOwnPropertyDescriptor(target, propertyKey) :类似于 Object.getOwnPropertyDescriptor()。如果对象中存在该属性,则返回对应的属性描述符, 否则返回 undefined.
  • Reflect.defineProperty(target, propertyKey, attributes):和 Object.defineProperty() 类似。如果设置成功就会返回 true
  • Reflect.ownKeys(target):返回一个包含所有自身属性(不包含继承属性)的数组。(类似于Object.keys(), 但不会受enumerable影响)。
  • Reflect.has(target, propertyKey) :判断一个对象是否存在某个属性,和 in 运算符 的功能完全相同。
  • Reflect.get(target, propertyKey[, receiver]):获取对象身上某个属性的值,类似于 target[name]。
  • Reflect.set(target, propertyKey, value[, receiver]):将值分配给属性的函数。返回一个Boolean,如果更新成功,则返回true。
  • Reflect.deleteProperty(target, propertyKey) :作为函数的delete操作符,相当于执行 delete target[name]。
  • Reflect.apply(target, thisArgument, argumentsList) :对一个函数进行调用操作,同时可以传入一个数组作为调用参数。和Function.prototype.apply() 功能类似。
  • Reflect.construct(target, argumentsList[, newTarget]):对构造函数进行 new 操作,相当于执行 new target(...args)。

六、receiver参数

Proxy的get、set捕获器有receiver参数,即指定接收者receiver,可以把它理解为函数调用过程中的this。那它存在的作用是什么呐?

请看下面示例:

const obj = {
    _name:'cj',
    age:18,
    get name(){     //将存取描述符写入对象体内
        return this._name    //this === obj
    },
    set name(newValue){
        this._name = newValue
    }
}

const objProxy = new Proxy(obj,{
    get:function(target,key){
        console.log(`监听到访问${key}属性`)
        return Reflect.get(target,key)
    },
    set:function(target,key,newValue){
        console.log(`监听到给${key}属性设置值`)
        Reflect.set(target,key,newValue)
    }
})

console.log(objProxy.name)

       通过objProxy.name去访问属性首先会进入get捕获器,然后通过Reflect会执行name存取描述符,在name存取描述符中,会通过this访问_name。

       这里进行了两次属性的访问,一次是objProxy的name属性访问,一次是obj的_name属性访问,那这里会打印几次?

答案是:一次

       因为objProxy.name访问是会触发捕获器的,这里会打9印一次。然后通过this访问_name是不会再触发捕获器的,因为捕获器只会当访问代理对象的属性时才会触发,而通过this访问,这里的this是指向obj的,而不是objProxy,所以不会触发捕获器。

       但是我们要做到通过this访问对象内部属性也要能触发捕获器,这个时候receiver参数就排上用场了。前面我们说receiver可以看作函数调用的this,那么当我们通过objProxy.name访问时,this就指向为objProxy代理对象了,将receiver传递给Reflect,再通过this访问就会触发捕获器了。这样使得无论是外部访问objProxy的属性还是obj内部通过this访问属性都会经过捕获器。

配合使用receiver

const obj = {
    _name:'cj',
    age:18,
    get name(){
        return this._name      //this === objProxy
    },
    set name(newValue){
        this._name = newValue
    }
}

const objProxy = new Proxy(obj,{
    get:function(target,key,receiver){
        console.log(`监听到访问${key}属性`,target[key],target)
        return Reflect.get(target,key,receiver)    //将receiver传递给Reflect.get
    },
    set:function(target,key,newValue,receiver){
        console.log(`监听到给${key}属性设置值`,target[key],target)
        Reflect.set(target,key,newValue,receiver)
    }
})

console.log(objProxy.name)   
//第一次打印:console.log(`监听到访问name属性`,'cj',{_name:"cj",age:18})   
//第二次打印:console.log(`监听到访问_name属性`,'cj',{_name:"cj",age:18})

       将receiver传递给Reflect,然后Reflect执行get存取描述符,这个时候的this就是指向objProxy这个代理对象了。通过this访问也就会触发捕获器了。

总结

        Proxy能很好的监听整个对象的变化,vue3放弃了Object.defineproperty改用Proxy,弥补了vue2响应式无法对引用数据类型的增添、删除无法监听到的缺陷。vue2只能通过提供的$setAPI手动将添加或删除的属性加入响应式中。

       想要了解vue3响应式,就得必须了解Proxy

Logo

前往低代码交流专区

更多推荐