前言
最近看 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
结语
如果喜爱或有所帮忙的话,心愿能点赞关注,激励一下作者。
如果文章有不正确或存疑的中央,欢送评论指出。