ES6的代理模式 Proxy

秦篆javascriptjavascript大约 5 分钟

Object.defineProperty

在vue3出世后,总会遇到一个新的面试题:说一些vue2与vue3的区别。作为一个前端开发,最先想到的当然就是响应式的更改,

基础原理

在vue2中,我们通过Object.defineProperty来实现数据的响应式:

let obj = {
    name:'luolj',
    age:18
}

Object.defineProperty(obj,'name',{
    get(){
        return '帅比'
    },
    set(newValue){
        console.log('名称已经被修改为:'+newValue)
    }
})

console.log(obj.name)
obj.name = 'luolj2'
console.log(obj.name)

上面的代码执行后会输出

帅比
名称已经被修改为:luolj2
帅比

诶,是不是很奇怪,明明已经修改了name属性,但是再次获得name属性时,却没有变化。这是因为Object.defineProperty只能监听对象的属性,而不能监听对象本身。 我们的get函数总是返回一个固定值,因为在attributes中无法使用this,如果使用obj.name的话,会造成递归调用以至内存泄露。

怎么解决这个问题呢?其实也相当简单,我们可以在外部添加一个提前定义好的量,比如这样:

let obj = {
    name:'luolj',
    age:18
}

let proxyName = obj.name

Object.defineProperty(obj,'name',{
    get(){
        return proxyName
    },
    set(newValue){
        proxyName = newValue
        console.log('名称已经被修改为:'+newValue)
    }
})

console.log(obj.name)
obj.name = 'luolj2'
console.log(obj.name)

这段代码现在就能够正确输出了

luolj
名称已经被修改为:luolj2
luolj2

vue2中是怎么使用的呢

我们都知道,在vue2中定义响应式变量,需要在data中定义,然后在vue实例中使用,如下:

let vm = new Vue({
    data(){
        return {
            name:'luolj',
            age:18
        }
    }
})

从源码来看

function initState (vm) {
    vm._watchers = [];
    var opts = vm.$options;
    if (opts.props) { initProps(vm, opts.props); }
    if (opts.methods) { initMethods(vm, opts.methods); }
    if (opts.data) {
        initData(vm);
    } else {
        observe(vm._data = {}, true /* asRootData */);
    }
    if (opts.computed) { initComputed(vm, opts.computed); }
    if (opts.watch && opts.watch !== nativeWatch) {
        initWatch(vm, opts.watch);
    }
}

走马观花的看一下,initData函数中,vue2新定义了的一个新的_data用来初始化数据,然后通过observe函数来监听这个_data对象,这个observe函数是vue2中的一个重要函数,用来监听对象的变化。

接下来的代码我就不一一介绍了,大家可以自行查看vue2的源码。从initData函数中,我们可以知道一点,当我们在vue2中对数据进行比如this.nameopen in new window = xxx操作时,实际上是在对_data对象进行操作,然后通过Object.defineProperty来监听这个_data对象的变化。

Proxy

在ES6中,我们有了一个新的代理模式Proxy,它可以监听对象本身,接下来我们来说一下怎么用,为什么用它。

Proxy的基本使用

先给出一段示例

let obj = {
    name:'luolj',
    age:18
}

const proxy = new Proxy(obj,{
    get(target,key,receiver){ //receiver是proxy实例
        return Reflect.get(target,key,receiver)
    },
    set(target: { name: string; age: number }, p: string | symbol, newValue: any, receiver: any): boolean {
        return Reflect.set(target,p,newValue,receiver)
    }
})

js小知识

Reflect是一个内置的对象,它提供拦截JavaScript操作的方法。这些方法与proxy handler方法相对应。Reflect不是一个函数对象,因此它是不可构造的。

在一些高级语言中,反射是指程序可以访问、检测和修改它本身状态或行为的一种能力。在JavaScript中,Reflect对象的设计目的是反映(reflect)ECMAScript语义的底层操作。

由于其接收一个receiver参数,能够在native code层面保证上下文的完整性,所以一般Proxy操作的拦截都会使用Reflect来进行。

如上面的代码所示,大家应该能够看出与Object.defineProperty的区别。

当然,Proxy还有很多其他的方法,比如apply、has、deleteProperty等,大家可以自行查看文档。

为什么使用Proxy

Proxy可以监听对象本身,而Object.defineProperty只能监听对象的属性。
(面试题:为什么属性更新无法重绘视图)
以上面的例子来说,假设我需要在obj执行 obj.like = 'sw',第一个例子就无法再次拦截到这个属性的变化,因为Object.defineProperty只能监听对象的属性,而不能监听对象本身。

数组变异 $set $delete $add

data() {
    return {
        arr: [1, 2, 3]
    }
}

this.arr[3] = 4 // 无法监听到 

事实上,这个问题并不是由于Object.defineProperty的问题,而是由于尤大在性能方面的考量放弃了,以下有段代码可以证明这个问题

function defineReactive(data, key, val) {
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
      get: function defineGet() {
        console.log(`get key: ${key} val: ${val}`);
        return val;
      },
      set: function defineSet(newVal) {
        console.log(`set key: ${key} val: ${newVal}`);
        val = newVal;
      }
  })
}

function observe(data) {
  Object.keys(data).forEach(function(key) {
    defineReactive(data, key, data[key]);
  })
}

let test = [1, 2, 3];

observe(test);

test[0] = 4 // set key: 0 val: 4

于是就有了一系列的$方法来解决这个问题,但是这个方法并不是很优雅,而Proxy就可以很好的解决这个问题。

let obj = {
    name:'luolj',
    age:18
}

const proxy = new Proxy(obj,{
    get(target,key,receiver){ //receiver是proxy实例
        return Reflect.get(target,key,receiver)
    },
    set(target: { name: string; age: number }, p: string | symbol, newValue: any, receiver: any): boolean {
        return Reflect.set(target,p,newValue,receiver)
    }
})
obj.name = 'luo'
proxy.name = 'luo2'
console.log(obj.name) // luo2
console.log(proxy.name) // luo2

无论是修改原始对象obj还是修改代理对象proxy,都能够触发proxy的set方法,这就是Proxy的优势所在。

弊端

proxy只能接收一个 extends object类型的对象,所以对于数组、Map、Set等类型的对象,我们需要自己进行处理,这就是Proxy的一个弊端。
比如数组:

let arr = [1, 2, 3]

const createProxy = (obj: any) => {
    if (Object.prototype.toString.call(obj) === '[object Object]') {
        return new Proxy(obj, {})
    } else {
        let Obj = {
            value: obj
        }
        return new Proxy(Obj, {})
    }
}

let newArr = createProxy(arr)
arr.push(4)
console.log(newArr) // [1, 2, 3, 4]

vue内部呢对于非object类型的数据,会进行特殊处理,比如数组,这样就能够监听到数组的变化。

下一次我们来说一下vue3中的响应式,副作用函数effect,trigger等源码实现。

上次编辑于:
贡献者: luolj