vue3核心原理:响应式数据系统

前言

以下这几个模块是vue3发布订阅系统和响应式数据的核心组成;事实上vue2也有相应的组成部分,只不过依附在vue实例上,但是vue3把相应的模块独立出来,不再依附于vue实例;

ref 模块

ref模块总体作用是对数据进行响应式处理,即进行数据劫持,监听setget操作;set操作内对比数据改变以便通知订阅者进行更新(使用trigger模块),get操作内则追踪依赖者(使用track模块),也就是所谓的依赖收集,这些依赖者也就是订阅者;画成流程图大致如下:

img

img

上图是对ref模块核心代码的大致功能划分,可以看出源码的逻辑很清晰;经过ref函数包裹后得到的数据结构如下:

1
2
3
4
5
6
7
8
9
export interface Ref<T = any> {
/**
* Type differentiator only.
* We need this to be in public d.ts but don't want it to show up in IDE
* autocomplete, so we use a private Symbol instead.
*/
[RefSymbol]: true
value: T
}

即得到的数据是一个含有value属性的对象,而属性值就是原始数据;为何要包裹成一个对象?原因就是JS中的原始类型值无法进行数据劫持以便构成响应式数据,因此只好包裹成对象。

effect 模块

effect模块是专门用来收集函数内的副作用,当然,这不是说函数所有的副作用都能用effect进行收集;effect能够收集的副作用仅限于响应式数据(即Ref对象或对数据进行类似Ref对象的劫持处理),一般是函数内get了响应式数据;

img

从上面这个流程图可以看出,effect模块主要包含三个函数:

  • effect:这个函数的作用实际上就是限定当前副作用发生的范围,可以简单地理解为标记当前副作用为活动状态,便于进行依赖收集
  • track:这个函数的作用就十分明显了,就是收集当前活动的副作用作为依赖,即增加订阅者
  • trigger:同理,这个函数就是用于通知订阅者进行更新

所以,effect模块简单地来说就是充当发布者和订阅者之间的桥梁;可以看下effect构建函数,逻辑十分清晰:

img

img

img

img

img

comupted 模块

只要理解了effect模块的作用,那就能很快理解computed模块的原理了;所谓的计算属性的核心就是一个getter函数,这个函数是天然的副作用,即这个getter函数必然会依赖其他响应式数据;既然是副作用函数,那就直接借助effect模块就能收集依赖和触发更新了,这一点同Ref对象很像,而从源码就可以看出,computed函数返回的数据就是一个特殊的Ref对象

img

img

img

img

reactive模块

这个模块从功能上看起来跟ref有点类似,都是构建响应式数据,但是两者之间还是有区别的,这个区别官方文档上也有说明;简单地来说:

  • ref函数适合对原始类型的变量进行响应式数据处理;
  • reactive函数则适合对对象类型的变量进行响应式数据处理;

基于reactive构造得到的响应式数据的特点可知:这种响应式数据只能通过对象属性形式生效,一旦将其中的属性通过解构或者是赋值剥离原对象,则会丧失其响应性;不过官方也提供了toRefs函数来解决这个问题,toRefs函数可以将reactive数据对象的每个属性值包装成Ref对象,因此被剥离原对象也可以保持响应性;

img

上图只是简单地描述了典型的对象被代理成响应式数据的过程,着重关注于setget的代理,其他的代理属性和类型可以查看源码;

img

上图就是reactive构建函数了,从其中逻辑可以看出构造reactive数据的关键就在于设置代理配置了,根据不同的参数及不同的对象类型就可以设置相应的代理配置,从而得到不同的代理效果;

代理配置源码中有一个亮点:在代理深层对象时,并不是一开始就是直接进行递归调用,遍历每层对象属性;而是采用惰性初始模式,延迟递归发生的时机,即把深层次对象的递归放入get代理函数中,因此只有第一次真正get访问到这个深层对象时才会执行初始化函数;

在程式设计中, 惰性初始是一种拖延战术。在第一次需求出现以前,先延迟创建物件、计算值或其它昂贵程序。[1]

这样做有什么好处?除了避免一开始就递归大量属性时的风险,源码注释[2]上也说了:

1
2
3
4
5
6
if (isObject(res)) {
// Convert returned value into a proxy as well. we do the isObject check
// here to avoid invalid value warning. Also need to lazy access readonly
// and reactive here to avoid circular dependency.
return isReadonly ? readonly(res) : reactive(res)
}

这种模式还能避免循环引用时所产生的堆栈溢出的情况,毕竟只有真正get到这个属性时才会执行初始化函数,因此即便对象属性中发生了循环引用,没有通过属性访问符来无限访问就不会引起一般情况下所产生的堆栈溢出;

上面这种用法其实在computed模块中也有类似的应用。

watch 模块

watch模块的本质就是监听响应式数据,当数据更新时执行回调函数;看到响应式数据和更新回调就立马能想到effect模块了,没错,watch模块内部就是构造了一个副作用,从而将副作用加入到响应式数据中的依赖列表中;

img

上面这个流程图就是对watch模块内部核心逻辑的简单概括;

watch模块中还有一个watchEffect函数,看起来好像是用于监听副作用对象的,但是我看源码这个监听好像并没啥用处?有点疑惑……

相关文档


  1. https://zh.wikipedia.org/wiki/惰性初始模式 ↩︎

  2. https://github.com/vuejs/vue-next/blob/d005b578b183f165929e1f921584ce599178cad6/packages/reactivity/src/baseHandlers.ts#L88 ↩︎