vue3核心原理:响应式数据系统
前言
以下这几个模块是vue3
发布订阅系统和响应式数据的核心组成;事实上vue2
也有相应的组成部分,只不过依附在vue
实例上,但是vue3
把相应的模块独立出来,不再依附于vue
实例;
ref 模块
ref
模块总体作用是对数据进行响应式处理,即进行数据劫持,监听set
和get
操作;set
操作内对比数据改变以便通知订阅者进行更新(使用trigger
模块),get
操作内则追踪依赖者(使用track
模块),也就是所谓的依赖收集,这些依赖者也就是订阅者;画成流程图大致如下:
上图是对ref模块核心代码的大致功能划分,可以看出源码的逻辑很清晰;经过ref
函数包裹后得到的数据结构如下:
1 | export interface Ref<T = any> { |
即得到的数据是一个含有value
属性的对象,而属性值就是原始数据;为何要包裹成一个对象?原因就是JS
中的原始类型值无法进行数据劫持以便构成响应式数据,因此只好包裹成对象。
effect 模块
effect
模块是专门用来收集函数内的副作用,当然,这不是说函数所有的副作用都能用effect
进行收集;effect
能够收集的副作用仅限于响应式数据(即Ref
对象或对数据进行类似Ref
对象的劫持处理),一般是函数内get
了响应式数据;
从上面这个流程图可以看出,effect
模块主要包含三个函数:
effect
:这个函数的作用实际上就是限定当前副作用发生的范围,可以简单地理解为标记当前副作用为活动状态,便于进行依赖收集;track
:这个函数的作用就十分明显了,就是收集当前活动的副作用作为依赖,即增加订阅者;trigger
:同理,这个函数就是用于通知订阅者进行更新;
所以,effect
模块简单地来说就是充当发布者和订阅者之间的桥梁;可以看下effect
的构建函数,逻辑十分清晰:
comupted 模块
只要理解了effect
模块的作用,那就能很快理解computed
模块的原理了;所谓的计算属性的核心就是一个getter
函数,这个函数是天然的副作用,即这个getter
函数必然会依赖其他响应式数据;既然是副作用函数,那就直接借助effect
模块就能收集依赖和触发更新了,这一点同Ref
对象很像,而从源码就可以看出,computed
函数返回的数据就是一个特殊的Ref
对象;
reactive模块
这个模块从功能上看起来跟ref
有点类似,都是构建响应式数据,但是两者之间还是有区别的,这个区别官方文档上也有说明;简单地来说:
ref
函数适合对原始类型的变量进行响应式数据处理;reactive
函数则适合对对象类型的变量进行响应式数据处理;
基于reactive
构造得到的响应式数据的特点可知:这种响应式数据只能通过对象属性形式生效,一旦将其中的属性通过解构或者是赋值剥离原对象,则会丧失其响应性;不过官方也提供了toRefs
函数来解决这个问题,toRefs
函数可以将reactive
数据对象的每个属性值包装成Ref
对象,因此被剥离原对象也可以保持响应性;
上图只是简单地描述了典型的对象被代理成响应式数据的过程,着重关注于set
和get
的代理,其他的代理属性和类型可以查看源码;
上图就是reactive
构建函数了,从其中逻辑可以看出构造reactive
数据的关键就在于设置代理配置了,根据不同的参数及不同的对象类型就可以设置相应的代理配置,从而得到不同的代理效果;
代理配置源码中有一个亮点:在代理深层对象时,并不是一开始就是直接进行递归调用,遍历每层对象属性;而是采用惰性初始模式,延迟递归发生的时机,即把深层次对象的递归放入get
代理函数中,因此只有第一次真正get
访问到这个深层对象时才会执行初始化函数;
在程式设计中, 惰性初始是一种拖延战术。在第一次需求出现以前,先延迟创建物件、计算值或其它昂贵程序。[1]
这样做有什么好处?除了避免一开始就递归大量属性时的风险,源码注释[2]上也说了:
1 | if (isObject(res)) { |
这种模式还能避免循环引用时所产生的堆栈溢出的情况,毕竟只有真正get
到这个属性时才会执行初始化函数,因此即便对象属性中发生了循环引用,没有通过属性访问符来无限访问就不会引起一般情况下所产生的堆栈溢出;
上面这种用法其实在computed
模块中也有类似的应用。
watch 模块
watch
模块的本质就是监听响应式数据,当数据更新时执行回调函数;看到响应式数据和更新回调就立马能想到effect
模块了,没错,watch
模块内部就是构造了一个副作用,从而将副作用加入到响应式数据中的依赖列表中;
上面这个流程图就是对watch
模块内部核心逻辑的简单概括;
watch
模块中还有一个watchEffect
函数,看起来好像是用于监听副作用对象的,但是我看源码这个监听好像并没啥用处?有点疑惑……