前言
最近看 Vue3 源码,看到 Vue 对 Map/Set 做了很多特殊处理,把他们身上的所有方法都又实现了一遍,引起了一些思考与尝试,写篇文章分享出来
先说一个新奇的发现:
Proxy 是无法直接拦截 Set/Map 的!因为 Set/Map 的方法必须得在它们自己身上调用
看到这句话你不禁会想 Vue 是如何代理他们的,继续看下去吧
方法的三种调用形式
本节探讨 Set/Map 它们实例方法的三种调用形式
这里不包括在它们示例身上调用,自己调用当然能正常运行
在 Proxy 对象上调用
用 Proxy 代理一个集合,不做任何拦截,然后调用 add 方法,寄!
const p = new Proxy(new Set(), {})
p.add(1)
// TypeError: Method Set.prototype.add called on incompatible receiver #<Set>
虽然没法运行方法,但好消息是 Proxy 能拦截到方法的读取,这是下文能够使用 Proxy 包装 Set/Map 的基础
const p = new Proxy(new Set(), {
get(target, key) {
console.log('get:', key)
return Reflect.get(target, key)
},
})
p.add(1) // get: add
// TypeError: Method Set.prototype.add called on incompatible receiver #<Set>
在继承集合的对象上调用
创建一个对象继承一个集合,尝试调用 add 方法,也是寄!
const obj = Object.create(new Set())
obj.add(1)
// TypeError: Method Set.prototype.add called on incompatible receiver #<Set>
const obj = {}
Object.setPrototypeOf(obj, new Set())
obj.add(1)
// TypeError: Method Set.prototype.add called on incompatible receiver #<Set>
在子类身上调用
难道就没有办法在其他对象身上调用 Map/Set 的方法了吗?
还是有的,就是它们的子类示例
class mySet extends Set {
constructor() {
super()
}
add(value) {
super.add(value)
console.log('终于成功运行了')
return this
}
}
let set = new mySet()
set.add(1) // 终于成功运行了
console.log(set) // mySet(1) [Set] { 1 }
结果
经过上述实验,我们知道了想要拦截 Set/Map,最简单的方式是为它们设置子类
但是,Vue 并没有采用这种方法,原因也很简单,class 关键期 IE13(Edge13)才出,而 Vue 想兼容到 IE12。
而且这一特性是 babel 解决不了的,就是垫不起来
所以呢,Vue3 还是选择用 Proxy 重写方法来解决,接下来让我们看看具体是怎么实现的
用 Proxy 包装 Set
实现思路
既然 Set/Map 的方法只能在原对象上调用,那我们就封装一套方法,先获取原对象,再在它们身上调用方法就好了
就像下面这样
const p = new Proxy(new Set(), {
get(target, key) {
if (key === 'add') return add // 返回自己实现的方法
return Reflect.get(target, key)
},
})
function add(value) {
const rawTarget = toRaw(this) // 获取代理的原对象
rawTarget.add(value) // 原对象再调用 add 方法
return this // add方法会返回集合本身
}
toRaw 是 Vue 实现的一个 api,用来获取代理对象的原对象
实现 toRaw
toRaw 实现起来也很简单,毕竟代理对象的拦截器是咱们自己写的,只要在其中定义一个特殊的属性,让拦截器返回原对象就行
const p = new Proxy(new Set(), {
get(target, key) {
if (key === '__v_raw') return target // 访问特殊属性,返回原对象
if (key === 'add') return add // 返回自己实现的方法
return Reflect.get(target, key)
},
})
// 获取原对象的方法
function toRaw(p) {
return p['__v_raw']
}
Vue 中考虑到多层代理嵌套的问题,所以源码中 toRaw 的实现是递归调用的,直至对象没有 '__v_raw'
属性
toRaw 实现后,add 函数就已经能够正常运行了
p.add(1)
p.add(2)
console.log(p)
// Proxy { 1, 2 } 浏览器控制台输出
// Set(2) { 1, 2 } node控制台输出
Vue 就是使用这一方式,实现了对 Set/Map 的代理
Vue 中的具体实现
在这里展示一部分 Vue3 的源码,主要是 reactive
方法中对 Set/Map 做的特殊处理
展开或修改了一些函数的调用,但逻辑不变
function reactive(target) {
let proxy // 代理对象
const type = Object.prototype.toString.call(target) // 获取类签名
// 对 Set 和 Map 特殊处理
if (type === '[object Map]' || type === '[object Set]') {
// 使用 collectionHandlers
proxy = new Proxy(target, collectionHandlers)
// 将代理对象设置到全局Map中,我们不具体实现
proxyMap.set(target, proxy)
}
return proxy
}
const collectionHandlers = {
get(target, key) {
if (key === '__v_raw') return target // 访问特殊属性,返回原对象
// 如果是Set/Map的原生方法,返回自己封装的方法
// 否则返回对象身上的属性
return Reflect.get(instrumentations.hasOwnProperty(key) ? instrumentations : target, key)
},
}
// 重写了Set/Map的所有原生方法和属性
const instrumentations = {
get,
set,
add,
has,
delete: deleteEntry,
clear,
forEach,
get size() {
return size(this)
},
}
instrumentations 中方法的重写代码就不展示了,简单总结一下,感兴趣的自行去查看源码
- 所有方法都是通过
toRaw(this)
获取了原对象,在其身上尝试调用方法。并且对所有传入的参数也解了代理rawKey = toRaw(key)
,以确保存入 Set/Map 中的都是原对象。 - 在执行
get
forEach
方法获取数据时,会再次使用reactive
包装 - 在
get
has
forEach
size
函数中跟踪依赖(track
) - 在
set
delete
clear
add
函数中触发扳机(trigger
) - Vue 还重写了迭代器属性/方法(
['keys', 'values', 'entries', Symbol.iterator]
),以确保迭代器产生的值都被reactive
包装记录
最后,Vue 对 Set/Map 代理后的结果是:真正存入的对象都是解代理后的原对象,但想从其中取出对象都会自动代理后再返回
其实重写的很多方法都做了两手准备,对已代理和未代理的参数都尝试执行了一遍,这是为了避免有小可爱先用 Set 存了代理对象,再将其传给 Vue
结语
如果喜欢或有所帮助的话,希望能点赞关注,鼓励一下作者。
如果文章有不正确或存疑的地方,欢迎评论指出。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。