从零到一打造 Vue3 响应式系统 Day 19 - Reactive:reactive 的基础实现

上一次我们提到:

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

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

Ref 与 Reactive 比较

Ref Reactive
数据结构 单一值 对象(多个属性)
依赖存储 直接在实例上 (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,从而导致内存泄漏。

JavaScript 复制代码
const targetMap = new WeakMap()

它的结构会长这样:

JavaScript 复制代码
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 来通知更新。

收集依赖

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

TypeScript 复制代码
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

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

触发更新

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

TypeScript 复制代码
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。

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

JavaScript 复制代码
//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

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

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

那我们要怎么做?

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

TypeScript 复制代码
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。

执行步骤

回顾我们今天:

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

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

相关推荐
king王一帅2 小时前
Incremark Solid 版本上线:Vue/React/Svelte/Solid 四大框架,统一体验
前端·javascript·人工智能
智航GIS7 小时前
10.4 Selenium:Web 自动化测试框架
前端·python·selenium·测试工具
前端工作日常7 小时前
我学习到的A2UI概念
前端
徐同保8 小时前
为什么修改 .gitignore 后还能提交
前端
一只小bit8 小时前
Qt 常用控件详解:按钮类 / 显示类 / 输入类属性、信号与实战示例
前端·c++·qt·gui
Mr -老鬼8 小时前
前端静态路由与动态路由:全维度总结与实践指南
前端
颜酱9 小时前
前端必备动态规划的10道经典题目
前端·后端·算法
wen__xvn9 小时前
代码随想录算法训练营DAY10第五章 栈与队列part01
java·前端·算法
大怪v10 小时前
前端佬们!!AI大势已来,未来的上限取决你的独特气质!恭请批阅!!
前端·程序员·ai编程
Mr -老鬼10 小时前
功能需求对前后端技术选型的横向建议
开发语言·前端·后端·前端框架