带你阅读Vue3.0响应式系统源码2-对象及数据结构分析

1,467 阅读6分钟

主题:对象及数据结构分析

1.全局实例对象

1)存放所有对象依赖筐的实例对象(在effect.ts文件顶部)

​ 存放所有对象依赖筐的实例对象只有一个,叫做targetMap,它是分成三层的树状数据结构,第一二层是Map数据类型,第三层是Set数据类型。它大体上是按照如下的结构进行组织的:

在讲解上面数据结构之前,先说明清楚两点:

  • Vue数据响应系统中,数据对象变成响应型数据和被观察的数是两码事。并且,响应性数据不一定是被观察的数据,被观察的数据一定是响应型数据
  • 数据变成响应型数据仅仅是加一层代理而已,并不会对原本数据进行任何处理

接下来逐一对上面的MapSet进行解释

  1. targetMap作为本数据结构的根目录结构:
    • 它的键存放着未代理的被观察的对象,一旦有数据被观察了,它就会被放到这个targetMap,所以targetMap的话最后存储所有被观察的对象(包括对象中的子对象),这里的对象包括对象、数组、容器。
    • 它的值是一个Map实例,存放着被观察对象的所有被观察的属性
  2. KeyToDepMap存放着某个被观察的对象中被观察的属性所对应依赖筐Dep
    • Vue2.0中,每个数据要变成一个响应型数据都要有一个Dep筐(那时候叫做依赖筐),这个对象每次被Watcher引用的时候,就会在这个Dep筐添加Watcher实例,所以它应该是一个容器类型。
    • Vue3.0的时候,Dep筐基本上和Vue2.0一样也是一个容器类型,存放着所有引用这个数据的Effect对象。
    • ps:如果你还没有理清Dep和Watcher之间的关系理清Vue2.0中的Dep和Watcher的关系
  3. Dep是一个存放着ReactiveEffect数据类型的一个集合(Set),这种数据类型的我们在下面成为Effect对象,使用Set而不使用Array的意图很简单,就是为了去重。

2)存放原生对象和代理对象关系的Map

​ 我们知道,使用ProxyReflect进行实现数据响应系统的话,原生数据变成响应型数据的话,仅仅是加一层代理。使用两种对象进行互转的原因有三个:

  • 防止重复代理,Map一个重要的作用是去重。
  • 进行代理后,面向用户(即程序员的只是一个代理对象),而原本的原生数据如果不进行处理的话,可能会丢失引用(但不会被GC,除非代理对被GC了)。
  • 内部有时候使用的是原生对象,有时候使用的是代理对象,如此复杂的情况下,需要有一种互转的机制。
// 使用WeakMap的原因可以去查一下GC对于弱引用的处理方式
const rawToReactive = new WeakMap<any, any>()
const reactiveToRaw = new WeakMap<any, any>()

3)依赖添加所需要的全局对象

​ 在前面说过,Effect对象相当于2.0中的Watcher对象,那么在进行依赖的绑定的时候,也就是执行第一次执行Effect方法的时候,会进行依赖的绑定。这跟2.0一样,需要两个全局变量:

  • activeEffect:当前活跃的Effect实例,在初始化Effect实例的时候,进行添加依赖的时候方便响应型数据进行获取effect对象从而实现依赖的绑定。
  • effectStack:存放Effect的数组,充当一个栈使用。

这两个全局变量跟2.0的两个全局变量的作用是一样的:

Dep.target = null
const targetStack = []

整个依赖的绑定流程如下:

不过首先要说明一下里面变量的情况:

  • activeEffect:当前正在执行的Effect实例。
  • Dep通过targetMap进行查找得到访问的数据的Dep筐,执行一个次activeEffect函数,可能有多个依赖绑定,也可能没有,完全取决于函数中对响应型数据的访问情况。

2.普通对象类型

​1)Effect类型对象

​ 上面多次提到了Effect,那么Effect是什么东西呢?笔者第一眼看到它,就觉得跟2.0的Watcher很像,以为它就是Watcher改个名字而已,但是当我看到整个packages/reactivity目录的代码后,去寻找创建观察者的三种方式(在理清Vue2.0中的Dep和Watcher的关系 这篇文章中,我根据自己的想法将Watcher分成三类,它们分别是)WatchAPI、render、computed。粗略看了三者的代码,发现Effect其实只是Watcher的一部分。它有以下的特点:

  • 它是一个带有属性的Function实例,Function就相当于Watcher的回调方法,每次修改被观察的数据后,就是执行这个回调函数。
  • deps存放回调函数引用的数据的dep筐,这跟2.0的Watcher和Dep关系一致
  • 前面两个属性是一定会有的,但是接下来这个属性很重要,但是Effect本身不提供,它就是scheduler方法,它决定了数据触发后,怎么进行执行数据回调,可以是懒求值、可以是微观队列执行等等,它本身不会提供的。而是由创建它的人进行提供。

Watcher比较:

  • Watcher就相当于一个大熔炉,它从观察者的诞生、依赖的添加、依赖的执行都有规定的方法,在实例化这个对象的时候只需要用一个标志位标志是哪种类型的观察者(代码中没有明确哪个标志位说明观察者的类型,但是可以有lazy、是否是渲染函数观察者这两个参数间接说明了它是有类型的)。
  • Effect:相当于一个插槽,调用者进行调用的时候,说明类型后还得自带执行方法才能进行。所以Effect更加轻便灵活,更加抽象。Vue3.0已经很明确将三种观察者对象进行分离了,所以Effect的存在自有它的道理。

2)Dep

​ 在Vue2.0中,Dep还是一个类呢,为什么到Vue3.0却只是一个Set呢?其实可以去观察一下Dep这个类,看看它是不是跟Set长得差不多,并且Dep的作用就是存放某个数据所有绑定的Watcher,它的底层还是用Array实现的,那为什么不用Set来代替(况且Set还能去重)呢

3.本系列解读传送门

1.绪论

2.对象及数据结构分析

3.响应型数据诞生

4.依赖绑定以及触发依赖的执行策略

5.总结

附录1.Vue3.0代理如何对数组的原生方法进行观察

附录2.Vue3.0响应数据对象的构建过程(必读)