Vue3源码阅读【番外篇】:为什么Proxy需要搭配Reflect来实现响应式?

1,259 阅读2分钟

前言

我们都知道vue3.x版本是通过 Proxy 来实现的,用以解决2.X的一些缺陷。

但是我最近通过看 vue 源代码发现它内部很多地方是 Proxy、Reflect搭配一起使用的,例:

image.png image.png image.png 我们通过上面几张图可以看到 get、set、deleteProperty、has、ownKeys都用了 Reflect

基于此,这就是我写这篇文章的缘由,那我们探索一下为什么要搭配这两使用。

ProxyReflect 概念

  • Proxy: 代理,可以通过代理对象完成对目标对象的拦截,并在拦截后进行过滤和改写等操作,支持的拦截操作共 13
  • Reflect: 反射,它提供拦截 JavaScript 操作的方法。这些方法与 Proxy 的方法一一对应,也是 13 种。

Proxy简单使用示例:

const person = {
    name: '张三',
    get nickName() {
        return `${this.name}是坏蛋`
    }
}
const personProxy = new Proxy(person, {
    get(target, key, receiver) {
        console.log('进来了吧') 
        return target[key]
    }
})
console.log(personProxy.nickName)
// 先打印了:进来了吧
// 然后打印了:张三是坏蛋

通过上面这个示例,我们发现他成功代理了,虽然我们没做什么处理。

不过 get 里面的 receiver 这个参数我们还没用到,receiver在这个时候表示代理对象(也就是personProxy)。

我们在 get 里面的return target[key],不能return receiver[key],否则会死循环。

我们再来个 Proxy 示例2:

const person = {
    name: '张三',
    get nickName() {
        console.log(this)
        return `${this.name}是坏蛋`
    }
}

const personProxy = new Proxy(person, {
    get(target, key, receiver) {
        return target[key]
    }
})

const person2 = {
    name: '李四'
}

Object.setPrototypeOf(person2, personProxy)

console.log(person2.nickName)

上面这个示例种的console.log(this)console.log(person2.nickName) 会打印上面呢?

我们希望的应该是 person2这个对象李四是坏蛋 是吧,但实际上没有如期打印。

首先person2上面没有nickName这个属性,所以他会去personProxy上面找,然后返回了 person 上面的 nickName属性,nickName中的this指向了person他自己。

所以console.log(this)打印出来的是 person 对象,console.log(person2.nickName)打印出来的是person中的name,也就是张三是坏蛋

那我们怎么才能让他按照我们设想的那样打印李四是坏蛋

我们来到第三个示例:

const person = {
    name: '张三',
    get nickName() {
        console.log(this)
        return `${this.name}是坏蛋`
    }
}

const personProxy = new Proxy(person, {
    get(target, key, receiver) {
        return Reflect.get(target, key, receiver)
    }
})

const person2 = {
    name: '李四'
}

Object.setPrototypeOf(person2, personProxy)
console.log(person.nickName)        // 张三是坏蛋
console.log(personProxy.nickName)   // 张三是坏蛋
console.log(person2.nickName)       // 李四是坏蛋
/*
*   三次this打印的顺序如下:
*   { name: '张三', nickName: [Getter] }
*   { name: '张三', nickName: [Getter] }
*   { name: '李四' }
* */

我们通过在 get 里面 return Reflect.get(target, key, receiver) 来实现按我们想要的那样打印出来。

我们给 Reflectget 传递第三个参数(Proxy中的receiver),然后他就会修改调用时的this指向(也就是把指向person修改为:指向 person2)。

我们引用阮老师中的 Reflect 代码片段来说明一下 receiver,例:

var myObject = {
  foo: 1,
  bar: 2,
  get baz() {
    return this.foo + this.bar;
  },
};

var myReceiverObject = {
  foo: 4,
  bar: 4,
};

Reflect.get(myObject, 'baz', myReceiverObject) // 8

如果name属性部署了读取函数(getter),则读取函数的this绑定receiver

最后

最后我们引用vue官网的描述来理解一下:

使用 Proxy 的一个难点是 this 绑定。我们希望任何方法都绑定到这个 Proxy,而不是目标对象,这样我们也可以拦截它们。值得庆幸的是,ES6 引入了另一个名为 Reflect 的新特性,它允许我们以最小的代价消除了这个问题 传送门