【Vue】浅析Vue的Proxy代理为什么使用了Reflect?

878 阅读6分钟

阅读前提

JsVue有最基本的了解,包括但不限于:

  • Object.definePropertyProxy的区别与使用
  • Vue的响应式系统的作用与实现,需要了解其中的数据结构、副作用函数等 如果对以上前置知识不太了解,可能阅读起来会有难度,本文会以最简单化形式呈现。

标题在说什么问题

首先是两个概念的理解:

  • Proxy:简单地说,使用 Proxy 可以创建一个代理对象。它能够实现对其他对象的代理,这里的关键词是其他对象,也就是说,Proxy 只能代理对象,无法代理非对象值,例如字符串、布尔值等。
  • 代理:指的是对一个对象基本语义的代理。它允许我们拦截并重新定义对一个对象的基本操作。 哪里用到了Reflect? 以下是对Vue源码中使用Proxy代理时涉及get的一个最简单的写法
const p = new Proxy(obj, { 
	// 拦截读取操作,接收第三个参数 receiver
	get(target, key, receiver) { 
		track(target, key) 
		// 使用 Reflect.get 返回读取到的属性值
		// 为什么不用 target[key] ?
		return Reflect.get(target, key, receiver) 
	}, 
	// 省略部分代码
})
p.name // 此时会触发get函数

但是,如果对getter函数有过了解的小伙伴会问:为什么Vue不直接使用target[key]进行返回? 这就是这个标题提出的疑惑,也是本文想探讨的内容。

在解决这个问题之前,首先需要简单回顾Vue的响应式数据和副作用函数是啥

响应式与副作用函数

实现响应式

假设我们有一个原始对象obj

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

我们要监视这个对象的操作,如何做呢?例如:

obj.name  = 'jay'

在js内部一定会设计一套操作能够得知:

  • obj是否有一个name属性,如果有,那么name的值是多少
  • 新值'jay'是否是合理的值,如果合理,那么obj.name设置为新值'jay' 至少在这一步上,js对于对象会有我们熟知的gettersetter拦截 那么js不妨把一些对象内部的接口暴露出来给程序员使用即可,就有了Object.defineProperty,就有了Proxy。这两个也是Vue2、Vue3实现响应式的主要方式。(二者的区别不在本文提及)

Vue实现响应式

Vue借助Proxy的对一个对象进行拦截的读取和设置操作后,便可以实现响应式,例如:

// 存储副作用函数的桶
const bucket = new Set() 
// 原始数据
const data = { text: 'hello world' } 
// 对原始数据的代理
const obj = new Proxy(data, {
	// 拦截读取操作
	get(target, key) { 
		// 将副作用函数 effect 添加到存储副作用函数的桶中 
		bucket.add(effect) 
		// 返回属性值
		return target[key]
	}, 
	// 拦截设置操作
	set(target, key, newVal) {
		// 设置属性值
		target[key] = newVal
		// 把副作用函数从桶里取出并执行
		bucket.forEach(fn => fn()) 
		// 返回 true 代表设置操作成功
		return true
	}
})
// 副作用函数
function effect() {
	document.body.innerText = obj.text 
}
// 执行副作用函数,触发读取
effect() 
// 1 秒后修改响应式数据
setTimeout(() => { obj.text = 'hello vue3' }, 1000)
  • 初次effect走到obj.text的时候触发Set操作,将effect存在桶中(实际数据结构会比这个更复杂:使用weakMap的key放置对象,使用map的key放置对象的属性值,使用set放置依赖副作用函数,不在本文提及)
  • 在最后一行定时器改变值之后,会触发obj对于textSet操作,并重新执行effect函数 以上是对Vue响应式副作用函数最简单的实现(具体优化与实现不在本文中提及)

所以为什么使用Reflect

回顾完vue的响应式和副作用函数后,回到本文最开始的代码,这里的返回值用了Reflect

const p = new Proxy(obj, { 
	// 拦截读取操作,接收第三个参数 receiver
	get(target, key, receiver) { 
		track(target, key) 
		// 使用 Reflect.get 返回读取到的属性值
		return Reflect.get(target, key, receiver) 
	}, 
})
p.name // 此时会触发get函数

什么是Reflect

Reflect:是一个全局对象,其下有许多方法

Reflect.get() 
Reflect.set() 
Reflect.apply() 
// ...
  • Reflect 下的方法Proxy 的拦截器方法名字相同,任何在 Proxy 的拦截器中能够找到的方法,都能够在 Reflect 中找到同名函数
  • 那么这些函数的作用是什么呢?拿 Reflect.get 函数来说,它的功能就是提供了访问一个对象属性的默认行为
  • Reflect.get 函数还能接收第三个参数,即指定接收者 receiver,你可以把它理解为函数调用过程中的 this
const obj = { foo: 1 } 
// 直接读取 
console.log(obj.foo) // 1 
// 使用 Reflect.get 读取
console.log(Reflect.get(obj, 'foo')) // 1
console.log(Reflect.get(obj, 'foo', {foo: 2})) // 2

使用target[key]的弊端

在这种场景下,副作用函数并不会重新执行

const obj = { 
	foo: 1, 
	get bar() { 
		return this.foo 
	} 
}
const p = new Proxy(obj, { 
	get(target, key, receiver) { 
		track(target, key) 
		// 返回属性值
		return target[key]
	},
})
effect(() => { console.log(p.bar) })  // 1
p.foo++

这里做了什么呢

  1. 为原始对象obj设了一个代理对象p
  2. 副作用函数注册时,读取到p.bar,发现其为一个访问器属性,于是执行getter函数,返回了this.foo
  3. 这里的this是谁呢?p.bar的返回值target[key]其实就是obj.bar(get函数的参数target:obj,key:'bar')所以这里的this指向的是原始对象obj
  4. 所以副作用函数读取到的是obj.foo,副作用函数跟原始对象obj响应上了,但是我们是通过代理对象p建立响应关系的呀~
  5. 所以我们p.foo++的时候,p并不知道要让这个副作用函数再次执行,所以effect不会重新执行,控制台没有新变化

Reflect.*方法响应代理对象

那该咋让副作用函数和代理对象p响应上呢?把代理对象的get修改一下

const obj = { 
	foo: 1, 
	get bar() { 
		return this.foo 
	} 
}
const p = new Proxy(obj, { 
	get(target, key, receiver) { 
		track(target, key) 
		// 返回属性值
		return Reflect.get(target, key, receiver) 
	},
})
effect(() => { console.log(p.bar) })  // 1
p.foo++ // 传入的receiver就是p这个对象

代理对象的 get 拦截函数接收第三个参数 receiver,它代表谁在读取属性 改成Reflect的话这里做了什么呢

  1. 为原始对象obj设了一个代理对象p
  2. 副作用函数注册时,读取到p.bar,发现其为一个访问器属性,于是执行getter函数,返回了this.foo
  3. 这里的this是谁呢?我们已经知道receiver是代理对象p,所以访问器属性bargetter 函数内的this指向代理对象p
  4. 所以副作用函数读取到的是p.foo,副作用函数跟代理对象p响应上了
  5. 所以我们p.foo++的时候,p知道要让这个副作用函数再次执行(set函数的作用),所以effect重新执行,在控制台输出2
  6. 本文标题问题得到答案

Proxy对象部署的所有内部方法

image.png 在其他的拦截操作中也能见到Reflect的影子

const p = new Proxy(obj, {
	// delete操作
	deleteProperty(target, key) { 
		return Reflect.deleteProperty(target, key)
	},
	// in操作
	has(target, key) {
		return Reflect.has(target, key) 
	}
	// ...
}) 

创建代理对象时指定的拦截函数,实际上是用来自定义代理对象本身的内部方法和行为的,而不是用来指定被代理对象的内部方法和行为的。

参考书籍

这是我在阅读《Vue.js设计与实现》第4、5章时因疑惑整理留下的一份笔记,供大家共同学习与讨论更正。