Vue3设计与实现共读-响应系统(三)

588 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第3天,点击查看活动详情

在上一节中,我们使用了set数据结构作为我们存储副作用函数的桶。这使得我们没有在副作用函数与被操作的的目标字段之间建立明确的联系。也就是说当我们读取属性时,我们并不能确定读取的是哪一个属性,都会把副作用函数收集到桶中去;而当设置属性时,又都会把桶中的副作用函数全部取出并执行。这是我们首先要解决的问题。

解决这个问题也很简单,既然我们现在的问题是副作用函数与被操作字段之间没有一一对应的联系,那我们只需要在副作用函数与被操作字段之间建立联系即可。这时我们便不能简单的使用set类型作为我们所需要的数据结构了。

在思考如何设计数据结构之前,我们先看一下下面的代码

effect(function effectFn(){
  document.body.innerText = obj.text
})

在这段代码中,存在着三个角色:

  • 被操作(读取)的代理对象 obj;
  • 被操作(读取)的字段名 text;
  • 使用 effect 函数注册的副作用函数effectFn;

如果target来表示一个代理对象所代理的原始对象,用key来表示被操作的字段名,用effectFn来表示被注册的副作用函数,那么这三个角色的关系如下所示:

  • target
    • key
      • effectFn

显然这是一种树形结构。

如果有两个副作用函数同时读取同一个对象的属性值:

effect(function effectFn1(){
	obj.text
})
effect(function effectFn2(){
	obj.text
})

那么对应的关系:

  • target

    • key

      • effectFn1
      • effectFn2

如果一个副作用函数中读取了同一个对象的两个不同的属性时:

effect(function effectFn(){
	obj.text1
	obj.text2
})

那么关系如下:

  • target

    • text1

      • effectFn
    • text2

      • effectFn

那如果在不同的副作用函数中读取了一个对象的两个不同的属性是:

effect(function effectFn1(){
	obj.text1
})
effect(function effectFn2(){
	obj.text2
})

那么关系如下:

  • target

    • text1

      • effectFn1
    • text2

      • effectFn2

综上,其实就是一个树形数据结构。这个联系建立起来之后,就可以解决了之前所提出的问题。

接下来就到了实现桶结构的时候了,我们需要使用WeakMap代替Set作为桶的数据结构。

// 存储副作用函数的桶
const bucket = new weakMap

然后修改我们之前写好的拦截器代码:

const obj = new Proxy(data, {
  // 拦截读取操作
  get(target, key) {
    // 没有 activeEffect ,直接 return
    if (!activeEffect) return
    // 根据 target 从桶中取得depsMap,它也是一个Map类型:key--> effects
    let depsMap = bucket.get(target)
    // 如果不存在depsMap,那么新建一个Map并与target关联
    if (!depsMap) bucket.set(target, (depsMap = new Map()))
    // 再根据key从depsMap中取得deps,它是一个Set类型
    // 里面存储着所有与当前key相关联的副作用函数:effects
    let deps = depsMap.get(key)
    // 如果deps不存在,同样新建一个Set并与key关联
    if (!deps) depsMap.set(key, (deps = new Set()))
    // 最后将当前激活的副作用函数添加到桶里
    deps.add(activeEffect)
    // 返回属性值
    return target[key]
  },
  // 拦截设置操作
  set(target, key, newVal) {
    // 设置属性值
    target[key] = newVal
    // 根据target从桶中取得depsMap,它是key->effects
    const depsMap = bucket.get(target)
    if (!depsMap) reutrn
    // 根据key取得所有副作用函数effects
    const effects = depsMap.get(key)
    // 执行副作用函数
    effects && effects.forEach(fn => fn())
  }
})

从这段代码中可以看出在构建数据结构时,分别使用了WeakMap,Map和Set:

  • WeakMap 由 target —> Map 构成;
  • Map 由 key —> Set 构成

其中 WeakMap 的键是原始对象target,WeakMap 的值是一个Map实例,而Map的键是原始对象target的key,Map的值是一个由副作用函数组成的Set。

图片.png

为了方便描述,我们将Set数据结构所有存储的副作用函数集合称之为key的依赖集合。

这里我们来解释一下为什么要使用WeakMap,这其实涉及WeakMap和Map的区别:

const map = new Map()
const weakmap = new WeakMap()

(function(){
	const foo = {foo:1}
	const bar = {bar:2}

	map.set(foo,1)
	weakmap.set(bar,2)
})()

我们先定义了map和weakmap常量,分别对应Map和WeakMap的实例。接着定义了一个立即执行的函数表达式(IIFE),在函数表达是内部定义了两个对象:foo和bar,这两个对象分别作为map和weakmap的key。当该函数表达式执行完毕后,对于对象foo来说,它仍然作为map的key被引用着,因此垃圾回收机制不会把它从内存中移除,我们仍然可以通过map.keys打印出对象foo。然而对于对象bar来说,由于weakmap的key是弱引用,它不影响垃圾回收器的工作,所以一但表达式执行完毕,垃圾回收器就会把对象bar从内存中移除,并且我们无法获得weakmap的key值,也无法通过weakmap取得对象bar。

也就是说,WeakMap对key是弱引用,不影响垃圾回收器的工作。据此,一但key被垃圾回收器回收,那么其对应的键和值就访问不到了。所以WeakMap经常用于存储那些只有当key所引用的对象存在时(没有被回收)才有价值的信息。如果target对象没有任何引用了,就说明用户不再需要它,垃圾回收器就会回收,如果Map代替了WeakMap,那么就算用户侧的代码对target没有了引用,这个target也不会被回收,可能会导致内存溢出。

最后我们对上面代码进行一些封装处理。在目前的实现中,当读取属性值时,我们直接在get拦截函数里编写把副作用函数收集到桶里的这部分逻辑,但更好的做法是将这部分逻辑单独封装到一个tarck函数中,我们也可以将触发副作用函数重新执行的逻辑封装到trigger函数中:

const obj = new Proxy(data, {
  // 拦截读取操作
  get(target, key) {
    // 将副作用函数activeEffect添加到存储副作用函数的桶中
    track(target, key)
    // 返回属性值
    return target[key]
  },
  // 拦截设置操作
  set(target, key, newVal) {
    // 设置属性值
    target[key] = newVal
    // 把副作用函从桶里取出并执行
    trigger(target, key)
  }
})

function track(target, key) {
  // 没有 activeEffect ,直接 return
  if (!activeEffect) return
  let depsMap = bucket.get(target)
  if (!depsMap) bucket.set(target, (depsMap = new Map()))
  let deps = depsMap.get(key)
  if (!deps) depsMap.set(key, (deps = new Set()))
  deps.add(activeEffect)
}

function trigger(target, key) {
  const depsMap = bucket.get(target)
  if (!depsMap) return
  const effects = depsMap.get(key)
  effects && effects.forEach(fn => fn())
}

分别把逻辑封装到track 和 trigger 函数内,会为我们接下来的处理带来极大的便利。