前言

最近看 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

结语

如果喜爱或有所帮忙的话,心愿能点赞关注,激励一下作者。

如果文章有不正确或存疑的中央,欢送评论指出。