Vue.js 3.0源码解析(微课视频版)
上QQ阅读APP看书,第一时间看更新

2.2 参数响应式

createApp()函数内通过reactive()函数将对象转换为响应式内容。下面围绕reactive介绍Vue3内核心的响应式数据处理逻辑。

Vue3通过Proxy对象实现响应式数据。采用代理的方式对目标对象进行拦截,进行一系列自定义操作,从而达到读取和修改目标对象,并触发对应钩子函数的目的。Proxy对象涉及两个参数:target和handler。target是被劫持的目标对象。handler是用于声明代理target的指定行为的对象,它支持的拦截操作一共有13种,包括getPrototypeOf、setPrototypeOf、isExtensible、preventExtensions、getOwnPropertyDescriptor、defineProperty、has、get、set、deleteProperty、ownKeys、apply和construct。其中,get方法用于拦截对象属性的读取。set方法用于拦截对象属性的设置,它返回一个布尔值,用于判断设置成功或失败。

与Vue2相比,除了代理方式不同以外,核心逻辑基本一致,读取属性值时收集依赖,设置属性值时更新派发,通过发布订阅者模式实现数据的响应式。

根据上述介绍可知,传入的data数据为一个对象,并且通过Proxy对象进行属性的拦截,因此reactive()函数的代码实现如下:

obj为传入的data对象,此时需代理的对象为{count:0},拦截方法为reactiveHandlers(),下面对该对象进行set和get拦截。

依赖收集和更新派发前,不能匿名收集对应的effect副作用函数,因为对象在派发更新的时候,无法知道与effect副作用函数的映射关系,所以在收集前需要通过一个队列将对象、属性和effect副作用函数进行映射。这样才能保证对象的属性更新时,能够准确定位effect副作用函数并执行。在这种情况下,reactiveHandlers()函数内部无论是get还是set均存在映射的逻辑。整个get和set的方法实现逻辑如下:

对于get方法,

(1)进行依赖收集。

(2)收集完成后返回需要的值。

对于set方法,

(1)派发更新。

(2)继续赋值操作。

针对get实现,如果有读取操作,则需要进行依赖收集。在Vue3中,依赖收集比较复杂,通过obj->key->effect的顺序建立依赖收集关系链,进而实现对象和effect副作用函数的关联关系。通过该关系既可以收集依赖,也可以对已收集的依赖进行缓存。

针对set实现,如果有修改操作,则需要触发effect副作用函数的执行。根据缓存的映射关系,调用effect副作用函数后再完成修改操作。

根据上述逻辑编写拦截函数,代码实现如下:

上述代码使用getDep()函数保存映射关系。通过getDep()函数获取对象的value值,在对象值读取完成后,进一步判断对象值是否为对象,若为对象则需要继续调用reactive递归遍历进行依赖收集,保证所有的嵌套对象都能受到代理。

注:因为依赖收集放到get执行阶段,所以可以节省初始化时间。

getDep()函数处理依赖收集和派发更新,该函数内部实现逻辑如下:

上述代码通过全局变量targetToHashMap来保存映射关系,此处targetToHashMap的类型是WeakMap,该类型是ES6新增的原生数据结构,关于WeakMap的具体特性不展开介绍,这里仅简单介绍WeakMap的用法。WeakMap的key必须为Object,并且提供delete、get、has和set方法。该类型有一个特性,对应的key销毁后可直接释放内存,因此采用该类型作为缓存对象,缓存内容删除后立即释放内存。根据WeakMap的特点,选择WeakMap类型缓存映射关系,无须关心内存释放等问题。

前面已经介绍过,为了实现target->key->effect的映射关系,全局缓存空间已声明,再通过get()方法判断target是否已缓存完成。若已完成,则直接跳过缓存target的步骤;若没有完成,则再定义一个target作为键,Map类型对象作为值,用于缓存dep变量。截至该步骤,WeakMap结构为{target:new Map()}。

注:Map对象也是以键值对的格式进行存储,与Object最大的不同是,Map的key可以是任何值。

此处选择Map结构作为键值对存储,也是考虑到key的变化,正好符合Map结构的特性。截至该步骤,Map结构为{key:effect}。

此时需要建立target和key之间的关系,此处先判断target的new Map()内是否有key的缓存,若没有则保存key,对应的值为effect副作用函数,进行该步操作后,结构为{target1:{key1:effect1}},通过两层键值对映射,建立起target->key->effect之间的关联关系。

effect副作用函数的内容在该函数内未体现,而是通过dep实例实现,该实例通过Dep类定义get和set方法,涉及get依赖收集和set派发更新方法,effect副作用函数的映射也在该Dep类内完成。下面查看该类实现逻辑:

上述Dep类实现如下:定义get和set拦截方法,get方法内通过depend方法依赖收集,完成依赖收集后返回对应值,set方法内将值更新,再通过notify方法进行派发更新。

仔细查阅上述代码可能会注意到,第一行定义了全局对象activeEffect,该对象被用作存储当前组件的effect副作用函数。Dep类的内部新定义Set结构subscribers用于存储effect副作用函数。

此处通过ES6的Set对象初始化,它的主要特性是去重和存储任意类型值。通过Set对象可以保存特殊类型数据并避免值重复问题。

当触发依赖收集时,判断activeEffect是否为空,若有effect副作用函数,则通过add()方法将该函数保存到subscribers对象内,完成依赖收集。

当触发派发更新时,通过forEach遍历subscribers对象,调用保存的effect副作用函数,完成派发更新通知。

此处经常提到effect副作用函数,关于该函数的作用将在2.3节进行介绍,通过该方法通知对应的组件进行patch等一系列的更新等操作。完成该步骤后,整个对象的响应式处理步骤介绍完毕。