关于前端:聊聊-Vue3-中对-SetMap-的处理-toRaw-的实现

35次阅读

共计 3222 个字符,预计需要花费 9 分钟才能阅读完成。

前言

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

结语

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

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

正文完
 0