ZuB1M1H.png

上一次我们提到:

  • 每个对象的每个属性都需要自己的 Dep
  • 如何建立 target.aDep 的对应关系?
  • 如何在不污染原始对象的情况下存储这个关系?

我们可以先来做一个简单的比较。

Ref 与 Reactive 比较

RefReactive
数据结构单一值对象(多个属性)
依赖存储直接在实例上 (this)需要一个外部的存储机制
依赖数量一个 ref 对应一个 Dep一个对象对应多个 Dep(每个属性一个)

匯出到試算表

Ref 可以用 this 来存储依赖,是因为它本身就是一个封装了 subssubsTail 属性的实例。

Reactive 的每个属性都需要自己的 Dep,那要存在哪里呢?这时候我们就可以使用 WeakMap 对象。

什么是 WeakMap?

  • WeakMap 是一种键值对集合 (key-value pairs)。
  • key 只能是对象(不能是字符串、数字、布尔值),value 可以是任意类型。
  • 弱引用 (weak reference) :如果一个对象只被 WeakMap 作为 key 使用,而程序中没有其他变量引用它,这个对象就会被垃圾回收 (GC) 自动清除。

看来它正好适合我们用来建立依赖的关联关系。

核心概念

我们使用 WeakMap 创建一个全局的 targetMap,它的三层嵌套结构如下:

  1. 第一层 targetMap (WeakMap)key 是原始的目标对象 targetvalue 是第二层的 depsMap{ target => depsMap }
  2. 第二层 depsMap (Map)keytarget 对象中的属性名 keyvalue 是第三层的 dep{ key => dep }
  3. 第三层 dep (Dep 实例) :依赖的容器,存储了所有订阅该属性变更的 effect{ subs, subsTail }

为何不直接用 Map?

因为如果使用普通的 MapMap 会一直保持对 target 对象的强引用。只要 target 还存在于 Map 中,GC 就无法回收它,即使程序其他地方已经不再使用这个 target,从而导致内存泄漏。

const targetMap = new WeakMap()

它的结构会长这样:

target = {
  a: 0,
  b: 1
}

targetMap = {
  [target]: { // WeakMap 的 key 是 target 对象本身
    'a': Dep,  // Map 的 key 是属性名 'a'
    'b': Dep   // Map 的 key 是属性名 'b'
  }
}

这样 Deptarget 的属性就有关系了。我们可以通过 target 找到对应的 Map,再通过属性 a 找到对应的 Dep 实例,这样就可以建立关联关系。

等到需要触发更新时,通过 target 找到对应的 Map,再通过 key 找到对应的 Dep 来通知更新。

day19-01.png

收集依赖

收集依赖分为首次收集和后续收集,所以我们可以这样写。

function track(target, key){
  if (!activeSub) return
  // 通过 targetMap 获取 target 的依赖合集 (depsMap)
  let depsMap = targetMap.get(target)

  // 首次收集依赖,如果之前没有收集过,就新建一个
  // key: target (obj) / value: depsMap (new Map())
  if (!depsMap) {
    depsMap = new Map()
    targetMap.set(target, depsMap)
  }

  // 获取属性对应的 Dep,如果不存在则新建一个
  let dep = depsMap.get(key)
  // key: key (property name) / value: new Dep()
  if (!dep) {
    dep = new Dep()
    depsMap.set(key, dep)
  }

  console.log(targetMap, dep)

  link(dep, activeSub)
}

可以 console.log 看一下 targetMapdep

day19-02.png 看起来确实是我们想的那样,接下来实现触发更新。

触发更新

触发更新时,我们理应去 targetMap 中寻找之前存储的 dep

function trigger(target, key){
  const depsMap = targetMap.get(target)
  // 如果 depsMap 不存在,表示没有任何依赖被收集过,直接返回
  if (!depsMap) return

  const dep = depsMap.get(key)
  // 如果 dep 不存在,表示这个 key 没有在 effect 中被使用过,直接返回
  if (!dep) return

  // 找到依赖,触发更新
  if (dep.subs) {
    propagate(dep.subs)
  }
}

接下来回头看我们的示例,初始化成功输出 0,一秒之后输出 1。

day19-03.png 看起来成功了。接下来我们来测试 reactivegetter 的响应式追踪:

//import { reactive, effect } from '../../../node_modules/vue/dist/vue.esm-browser.js'
import { reactive, effect } from '../dist/reactivity.esm.js'

const state = reactive({
  a: 0,
  get count(){
    return this.a
  }
})
effect(() => {
  console.log(state.count)
})

setTimeout(() => {
  state.a = 1
}, 1000)

预期结果是先输出 0,一秒后输出 1。但实际执行后,我们发现只有初始的 0 被输出,state.a = 1 的更新并未触发 effect

通过在 track 函数中输出 (target, key),发现只有 count 属性被追踪了,a 属性并没有。

原因在于 return this.a。在 getter 内部,this 默认指向的是原始的 target 对象,而不是我们的 proxy 对象。

因此 this.a 的取值过程绕过了 Proxyget 处理器,a 属性的依赖自然也无法被收集。

JavaScript

const state = reactive({
  a: 0,
  get count() {
    return this.a  // 这里的 this 应该指向谁?
  }
})

它应该指向我们的 Proxy 对象而不是原始对象,这样它在触发 getter 的时候,才会再次进入 Proxyget 处理器去执行 track(target, key)

那我们要怎么做?

Proxyhandler 提供了第三个参数 receiver,它指向的就是 proxy 对象本身。因此我们只需要将它传递给 Reflect.get 就可以修正 this 的指向。

function createReactiveObject(target) {
  // reactive 只处理对象
  if (!isObject(target)) return target

  // 创建 target 的代理对象
  const proxy = new Proxy(target, {
    get(target, key, receiver) { // 接收第三个参数 receiver
      // 收集依赖:绑定 target 的属性与 effect 的关系
      track(target, key)
      // 将 receiver 传给 Reflect.get
      return Reflect.get(target, key, receiver)
    },
    set(target, key, newValue, receiver) { // 接收第四个参数 receiver
      const res = Reflect.set(target, key, newValue, receiver)
      // 触发更新:通知之前收集的依赖,重新执行 effect
      trigger(target, key)
      return res
    }
  })

  return proxy
}

这样我们就完成了,也可以看到初始化时 console.log 输出 0,一秒之后输出 1。

执行步骤

day19-04.png 回顾我们今天:

  • 我们引入了以 WeakMap 为核心的 targetMap 数据结构,解决了在不污染原始对象的前提下,为多属性对象管理各自依赖的问题。
  • 我们实现了与 targetMap 配套的 tracktrigger 函数。
  • 我们利用 Proxyreceiver 参数,修正了 getterthis 指向的问题。

想了解更多 Vue 的相关知识,抖音、B站搜索我师父「远方os」,一起跟日安当同学。

本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:[email protected]