Vue3.2x源码解析(三):深入响应式原理
Vue3.2x源码解析(三):深入响应式原理
本节将深入理解vue3的响应式系统构成。
我们知道vue2的响应式数据依赖于原生方法Ojbect.defineProperty,因为这个方法在追踪集合数据类型变化有缺陷,所以vue3替换为了ES6的proxy方法,这也是本节讨论的重点。
vue2的响应式系统由三大核心模块构成:Observer、Dep、Watcher。
vue3的响应式系统同样是由三个核心模块构成:Data,Dep,Effect。
vue3中watcher变成了effect,虽然effect的实现及定义都发生了变化,但是它们的作用是类似的:都是为了执行副作用函数钩子。在阅读源码的时候,你可以以vue2的watcher进行参考对比,方便理解。
1,Data
data即响应式数据,vue3的响应式API比较多,但ref/reactive是其核心方法,其他API基本都以这两个为基础派生而来。所以我们在分析vue3如何定义响应式数据时,主要就是分析这两个API的源码。
一,ref
// packages/reactivity/src/ref.ts
# 定义响应式数据API
function ref(value?: unknown) {
return createRef(value, false)
}
继续查看createRef源码:
# 创建ref的函数
function createRef(rawValue: unknown, shallow: boolean) {
// 如果传入的value已经是ref数据,直接返回
if (isRef(rawValue)) {
return rawValue
}
// 新建一个ref实例 并返回
return new RefImpl(rawValue, shallow)
}
继续查看RefImpl源码:
# Ref类
/**
* value:可以为基本类型,也可以为集合类型
* 1,如果为基本类型,则将value直接赋值给_value属性
* 2,如果为集合类型,则通过toReactive转换为响应式数据后,再赋值给_value属性
*/
class RefImpl<T> {
// 存储当前值
private _value: T
private _rawValue: T
# dep属性默认为undefined,会在get的依赖收集时初始化为dep实例即依赖容器,存储effect元素
public dep?: Dep = undefined
// 是否是ref类型数据
public readonly __v_isRef = true
// public readonly __v_isShallow: boolean 属性值为第二个参数
constructor(value: T, public readonly __v_isShallow: boolean) {
// 初始化两个私有属性值
// 原value值
this._rawValue = __v_isShallow ? value : toRaw(value)
// 新value值: 不是浅层的就转化为深层响应式数据
this._value = __v_isShallow ? value : toReactive(value)
}
# ref类的核心是:定义了一个访问器value属性,获取value返回的是this._value
get value() {
// 收集依赖
trackRefValue(this)
return this._value
}
set value(newVal) {
// 判断是否为浅层/只读
const useDirectValue = this.__v_isShallow || isShallow(newVal) || isReadonly(newVal)
newVal = useDirectValue ? newVal : toRaw(newVal)
# 校验值是否变化
if (hasChanged(newVal, this._rawValue)) {
// 再次对两个私有属性值进行更新;
this._rawValue = newVal
# 重点:如果是浅层/只读的:就不会进行深层响应式创建 【数据类型的判断在toReactive方法里面】
this._value = useDirectValue ? newVal : toReactive(newVal)
// 触发依赖(副作用函数)
triggerRefValue(this, newVal)
}
}
}
根据前面的源码可以看出,ref的原理其实比较简单,一句话理解就是通过ref函数创建并返回了一个ref实例对象,同时ref实例内部定义了几个重要的属性,通过这些属性,我们可以创建出不同需求的响应式数据。
当然其中最核心还是定义了一个访问器属性value,我们对ref数据的变化操作都是通过value属性来进行实现,同时我们也可以发现ref数据是在getter中收集依赖,在setter中触发依赖,这和vue2的思想是完全一致。
其实在这里我们也是可以明白一个观点:vue3的源码虽然更多更抽象了,但其很多功能的使用方法和实现思路还是和原来一致的,这也是框架升级的一个重要特点,避免用户在使用新版本框架时产生较大的割裂感。
在对ref数据设置新值的时候,会对新值进行类型校验:如果是原始类型就直接赋值,如果是集合类型就会通过reactive方法创建proxy响应式代理对象并返回,这个逻辑都是在toReactive函数内部处理的:
// packages/reactivity/src/reactive.ts
// 如果使用ref创建响应式时:传入了对象类型参数,内部就会使用reactive方法生成proxy响应式代理对象
export const toReactive = <T extends unknown>(value: T): T =>
isObject(value) ? reactive(value) : value
综上所述:使用ref创建响应式数据时,如果传入原始类型数据,则直接赋值即可。如果传入的是引用类型数据,则需要通过reactive方法创建proxy代理对象再赋值,下面我们就开始分析针对对象类型的响应式数据创建。
二,reactive
// packages/reactivity/src/reactive.ts
# 定义响应式数据API
function reactive(target: object) {
// 如果是只读数据,直接return
if (isReadonly(target)) {
return target
}
// 创建响应式数据对象
return createReactiveObject(
target,
false,
mutableHandlers,
mutableCollectionHandlers,
reactiveMap
)
}
继续查看createReactiveObject源码:
# 创建响应式对象方法
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>,
proxyMap: WeakMap<Target, any>
) {
// 如果target不是对象,直接返回目标,不做任何操作
if (!isObject(target)) {
return target
}
# 如果目标已经是proxy代理对象,直接返回
if (
target[ReactiveFlags.RAW] &&
!(isReadonly && target[ReactiveFlags.IS_REACTIVE])
) {
return target
}
// 从proxyMap中查询目标target
# 如果目标已经存在对应的:proxy代理对象,直接返回对应的proxy对象【拒绝重复新建】
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
// 获取传入target的类型【基本类型视为无效类型】
const targetType = getTargetType(target)
// 如果目标为无效类型,不会进行响应式转换,直接返回
if (targetType === TargetType.INVALID) {
return target
}
# 新建proxy代理对象
const proxy = new Proxy(
target,
// 判断传入的target是不是map,set集合类型,传入的不同的handlers处理对象
// collectionHandlers:map/set ; baseHandlers: obj/list
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)
# 在map结构里,添加一对键值,存储目标与代理【作用是防止重复新建】
proxyMap.set(target, proxy)
// 返回代理对象,即响应式数据
return proxy
}
根据上面的源码可以看出,reactive方法只能对对象类型进行代理转换,如果传入原始类型数据,不会有任何操作。同时在创建proxy代理对象的之前,还做了一系列的优化校验,比如proxyMap变量是一个map结构,存储的键值都是对应的target与proxy,每次新建后都会存入一对键值。而在新建之前可以在这个map结构中进行查询是否存在已有的proxy,如果存在就直接返回不再新建,避免重复创建,优化框架的性能。
去掉这些校验:整个方法就是创建了一个proxy代理对象并返回,创建代理很简单,复杂的是拦截对象的设置。
# 重点:baseHandlers
const proxy = new Proxy(target, baseHandlers)
注意:我们这里只展开分析常规的obj/arr类型的baseHandlers,针对map/set集合类型的有兴趣可以去阅读源码。
baseHandlers是处理程序对象,是创建proxy的核心,它可以拦截针对target的所有基本语义操作,它是vue3对象类型实现响应式功能的核心,下面我们就继续分析baseHandlers源码:
/**
* Vue3的响应式核心原理:
* 利用proxy与Reflect的API,在get/has/ownKeys中收集依赖,在set/delete中触发依赖
*/
// packages/reactivity/src/baseHandlers.ts
# 处理程序对象handlers【针对普通对象】
const mutableHandlers: ProxyHandler<object> = {
// 五个捕获器obj/arr
get, // createGetter
set, // createSetter
deleteProperty,
// 通过has拦截函数实现对 in 操作符的代理:
has,
// 通过ownKeys拦截函数代理for in循环
// 使⽤for...in 循环遍历数组与遍历常规对象并⽆差异,因此同样可以使⽤ownKeys拦截数组
ownKeys
}
注意:我们这里只分析常规的mutableHandlers对象,它是基础也是核心,其他的只读响应式,浅层响应式更简单。
mutableHandlers是作用于常规对象和数组的处理程序对象,它里面有五个捕获器,这五个捕获器拦截了针对目标对象的所有增删改查操作。我们要理解vue3的响应式实现,就得理解每个捕获器的内部实现原理。
(一)get
const get = /*#__PURE__*/ createGetter()
// get:默认是一个getter
function createGetter(isReadonly = false, shallow = false) {
# 在获取属性值的操作中被调用
return function get(target: Target, key: string | symbol, receiver: object) {
// 如果get获取的是proxy对象的几个内置属性:直接返回对应值
if (key === ReactiveFlags.IS_REACTIVE) {
return !isReadonly
} else if (key === ReactiveFlags.IS_READONLY) {
return isReadonly
} else if (key === ReactiveFlags.IS_SHALLOW) {
return shallow
} else if (
key === ReactiveFlags.RAW &&
receiver ===
(isReadonly
? shallow
? shallowReadonlyMap
: readonlyMap
: shallow
? shallowReactiveMap
: reactiveMap
).get(target)
) {
return target
}
# 下面开始常规的key逻辑:
// 判断目标是不是数组类型
const targetIsArray = isArray(target)
if (!isReadonly) {
# 拦截数组的部分原生API,重写
// 如果是数组,并且key为数组的操作API,则进行拦截,重写对应的操作方法,并且加入依赖收集
// ['includes', 'indexOf', 'lastIndexOf'] ['push', 'pop', 'shift', 'unshift', 'splice']
if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
// 返回数组API读取的结果res
return Reflect.get(arrayInstrumentations, key, receiver)
}
if (key === 'hasOwnProperty') {
return hasOwnProperty
}
}
// 常规obj/arr的key读取:
# 核心原理API就是Reflect.get
const res = Reflect.get(target, key, receiver)
// 添加判断:如果key的类型是symbol,???
if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
return res
}
if (!isReadonly) {
# 收集依赖
track(target, TrackOpTypes.GET, key)
}
// 浅层响应式:直接返回res
if (shallow) {
return res
}
# 如果读取的结果是一个ref类型数据
if (isRef(res)) {
// ref unwrapping - skip unwrap for Array + integer key.
# 如果是数组 且 Key为整数即索引:直接返回res 否则返回ref.value
return targetIsArray && isIntegerKey(key) ? res : res.value
}
# 重点:这里key读取的最常见的两种结果就是obj和原始值;
// 深层响应式:如果读取的结果是一个obj,我们需要用reactive把res包装为响应式后再返回
# 每一层的访问结果 都会被reactive包装后返回
if (isObject(res)) {
// 深只读readonly
return isReadonly ? readonly(res) : reactive(res)
}
// 读取的结果是一个原始值,直接返回
return res
}
}
get捕获器对应的反射API方法为Reflect.get()。在get捕获器中有比较多的逻辑校验,首先就是对数组的校验,如果是数组类型,并且是数组原生API的操作读取,就进行拦截,重写为新定义的方法,并且在方法内部加入依赖收集。然后就是对常规obj/arr的key读取,使用了Reflect.get这个方法,这个方法是get捕获器的核心,读取目标对象的数据。
mutableHandlers处理程序对象的五个捕获器,每个捕获器内部都依赖于Reflect对象的对应API方法,这些方法与捕获器相互配合实现了Vue3的响应式核心。
最后要重点注意一下对读取结果的处理,如果是原始类型的数据直接返回,如果是对象类型的数据则需要使用reactive方法包装为响应式数据后再返回。
(二)set
const set = /*#__PURE__*/ createSetter()
function createSetter(shallow = false) {
# 在设置属性值的操作中被调用
return function set(target: object,key: string | symbol,value: unknown,receiver: object): boolean {
# 保存旧值
let oldValue = (target as any)[key]
// 如果是只读的且 旧值为ref数据,禁止设置,直接return
if (isReadonly(oldValue) && isRef(oldValue) && !isRef(value)) {
return false
}
if (!shallow) {
if (!isShallow(value) && !isReadonly(value)) {
oldValue = toRaw(oldValue)
value = toRaw(value)
}
if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
oldValue.value = value
return true
}
} else {
// in shallow mode, objects are set as-is regardless of reactive or not
}
# 针对key的存在进行判断
// isIntegerKey:是否为整数key,即数组的索引
// const hadKey = Number(key) < target.length 结果为布尔值
// const hadKey = hasOwn(target, key) 结果为布尔值
const hadKey =(isArray(target) && isIntegerKey(key)) ?
# 数组情况,key小于length说明在数组中存在
Number(key) < target.length
# 对象情况:是否存在属性key
: hasOwn(target, key)
# 常规key的value设置
// 核心原理API就是Reflect.set
const result = Reflect.set(target, key, value, receiver)
// toRaw(receiver) 判断receiver必须是target的代理对象才触发更新,如果是原型链上的则不触发更新,避免性能浪费
if (target === toRaw(receiver)) {
# 新增和修改:都要触发依赖(即:dep依赖容器中收集的effect实例)
// 区分ADD和SET,执行不同的触发逻辑
if (!hadKey) {
// 新增
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) {
// 修改
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
}
// 返回 true 表示设置成功;返回 false 表示失败
return result
}
}
set捕获器对应的反射API方法为Reflect.set(),在set捕获器中,首先校验了只读数据和ref数据,如果是这两种情况,则禁止设置。然后需要判断key有没有存在于目标对象之中。如果存在则为修改操作,如果不存在则为新增操作,区分不同的操作逻辑,需要执行不同的依赖触发逻辑。
(三)deleteProperty
# delete 操作符的代理
// 删除一个属性key
function deleteProperty(target: object, key: string | symbol): boolean {
# 校验target是否存在属性key
const hadKey = hasOwn(target, key)
const oldValue = (target as any)[key]
// 核心原理API:Reflect.deleteProperty
// 删除成功返回true
const result = Reflect.deleteProperty(target, key)
# 只有当被删除的属性是对象⾃⼰的属性并且成功删除时,才触发更新
if (result && hadKey) {
// 触发依赖
trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
}
return result
}
deleteProperty捕获器对应的反射 API 方法为Reflect.defineProperty(),首先校验目标对象是否存在属性key,不存在则会删除失败,返回false。只有存在属性key且删除成功的情况下,才会触发依赖。
(四)has
# in 操作符的代理
// 判断obj中有没有属性key
function has(target: object, key: string | symbol): boolean {
// 核心原理API:Reflect.has
const result = Reflect.has(target, key)
if (!isSymbol(key) || !builtInSymbols.has(key)) {
# 收集依赖
track(target, TrackOpTypes.HAS, key)
}
return result
}
has对应的反射 API 方法为 Reflect.has()。
(五)ownKeys
# for in循环,Object.keys()的代理
function ownKeys(target: object): (string | symbol)[] {
// 如果target是数组,则使⽤length属性作为 key 并建⽴响应联系
track(target, TrackOpTypes.ITERATE, isArray(target) ? 'length' : ITERATE_KEY)
return Reflect.ownKeys(target)
}
ownKeys对应的反射 API 方法为 Reflect.ownKeys()。
综上所述:这五个捕获器就是mutableHandlers处理程序对象的全部内容,通过这五个捕获器,reactive定义的响应式数据对象:可以拦截目标对象target的所有增删改查操作,并在相应的操作中收集依赖与触发依赖,以此为基础实现了vue3的响应式功能核心。
2,Dep
首先我们先分析dep的定义和创建:
# 定义了dep的类型
// &:交叉类型:将多个类型合并成一个类型,该类型具有所有类型的特性(取他们类型的合集)
// 相当于and符号
type Dep = Set<ReactiveEffect> & TrackedMarkers
ReactiveEffect和TrackedMarkers的源码这里省略。
# 创建dep实例:依赖容器
// vue2收集的是watcher实例, vue3收集的是各种effect实例
// ReactiveEffect[]和string[]一样:代表effect数组:意思传入的是一个数组,数组的每个元素都是effect实例
const createDep = (effects?: ReactiveEffect[]): Dep => {
// effects ?参数是可选的
// 当没有参数时:这里的dep = Set(0) { w: 0, n: 0 },代表有没有元素,只有两个属性
// 当有参数时:假如这里的dep = Set(3) {{…}, {…}, {…} w:0, n:0},代表有三个元素,同时也有w,n属性
const dep = new Set<ReactiveEffect>(effects) as Dep
// set也是对象,可以给他定义属性
dep.w = 0
dep.n = 0
# dep实例是一个set数据结构
return dep
}
dep的源码非常少也比较简单,根据上面的源码我们可以看出,dep就是一个set集合结构,里面存储的是effect实例,同时定义了w,n两个属性。dep的作用依然和原来一样:专门用于收集依赖【effect实例】的容器。
下面我们再去分析使用dep的地方,是如何收集依赖的。
3,Effect
effect在vue3称为副作用,其实它的作用和vue2的watcher是类似的,在阅读源码的时候可以参考对比。在vue3里面,dep就是收集的effect实例,最终的目的就是:在响应式数据变化的时候,执行effect实例中的回调函数。
下面我们分别来分析ref/reactive响应式数据是如何收集和触发依赖的:
一,ref
我们回到之前ref是如何收集和触发依赖的:
class RefImpl<T> {
get value() {
// 收集依赖
trackRefValue(this)
}
set value() {
// 触发依赖
triggerRefValue(this, newVal)
}
}
(一)收集依赖
在查看trackRefValue源码之前,有两个全局变量我们需要理解一下:
# 默认需要收集
export let shouldTrack = true
# 创建一个全局的activeEffect,存储当前创建/运行的组件renderEffect实例
export let activeEffect: ReactiveEffect | undefined
继续查看trackRefValue源码:
// 追踪ref值
function trackRefValue(ref: RefBase<any>) {
# activeEffect为undefined时无法执行,我们需要去查看activeEffect的赋值地方
if (shouldTrack && activeEffect) {
ref = toRaw(ref)
...
# createDep() 创建了一个dep实例,用于收集依赖
// 刚开始ref.dep=undefined; 第一次收集依赖,初始化ref.dep为set集合对象
// dep = Set(0) { w: 0, n: 0 },set0代表它没有元素,只有两个属性
trackEffects(ref.dep || (ref.dep = createDep()))
}
}
注意:文中的源码都会省略部分DEV环境下的代码。
很明显shouldTrack默认允许追踪的,但是activeEffect默认undefined,我们需要去查看它在什么情况下才会被赋值,只有在同时满足两个条件的情况下,trackEffects才会执行收集逻辑。
同时我们可以看见activeEffect被定义为ReactiveEffect类型,我们需要去查看ReactiveEffect源码:
/**
* effect类
* 相当于vue2的watcher
*/
# 重点:effect类
class ReactiveEffect<T = any> {
active = true
// 存储dep实例: [dep, dep, dep] dep实例:Set(1) {ReactiveEffect n:0 w:0 size: 1}
deps: Dep[] = []
parent: ReactiveEffect | undefined = undefined
computed?: ComputedRefImpl<T>
// 允许递归
allowRecurse?: boolean
// 延迟暂停
private deferStop?: boolean
onStop?: () => void
// dev only 省略
constructor(
# 回调函数Fn:和watcher实例中的fn一样,最终都是为了执行这个回调函数
public fn: () => T,
# 调度程序scheduler:非常重要,调度函数决定了如何执行fn
public scheduler: EffectScheduler | null = null,
// 作用域
scope?: EffectScope
) {
// 记录effect作用域
recordEffectScope(this, scope)
}
# 执行回调函数fn:返回值为fn的执行结果
run() {
if (!this.active) {
return this.fn()
}
let parent: ReactiveEffect | undefined = activeEffect
let lastShouldTrack = shouldTrack
while (parent) {
if (parent === this) {
return
}
parent = parent.parent
}
try {
this.parent = activeEffect
# 设置activeEffect为 当前effect实例
activeEffect = this
shouldTrack = true
trackOpBit = 1 << ++effectTrackDepth
if (effectTrackDepth <= maxMarkerBits) {
initDepMarkers(this)
} else {
cleanupEffect(this)
}
return this.fn()
} finally {
if (effectTrackDepth <= maxMarkerBits) {
finalizeDepMarkers(this)
}
trackOpBit = 1 << --effectTrackDepth
activeEffect = this.parent
shouldTrack = lastShouldTrack
this.parent = undefined
if (this.deferStop) {
this.stop()
}
}
}
stop() {
// stopped while running itself - defer the cleanup
if (activeEffect === this) {
this.deferStop = true
} else if (this.active) {
cleanupEffect(this)
if (this.onStop) {
this.onStop()
}
this.active = false
}
}
}
ReactiveEffect类和Vue2的watcher类非常类似:ReactiveEffect类生成effect实例,effect实例收集dep实例,dep实例又收集effect实例。
同时我们发现activeEffect是在run函数中被赋值,只有run函数被调用了,activeEffect才会被赋值,ref数据的trackEffects才会执行收集逻辑。所以我们必须要理解activeEffect被赋值的这个逻辑过程。
解析之前,我们回顾上一节的组件初始化过程:组件在创建renderEffect实例之前,会先调用setup函数完成组件初始化。而通过上一节我们知道Vue3的响应式数据都是在setup调用过程中初始化的,包括计算属性以及watch。所以在组件的renderEffect创建之前,组件内的ref,reactive,computed,watch就已经完成了初始化。
一,首先ref/reactive一定是最开始初始化的,因为computed和watch一般都需要依赖于响应式数据,所以我们的computed和watch一般都会定义在响应式数据之后,所以ref/reactive在初始化创建完成之时,是没有收集任何依赖的,因为他们没有还没有被引用,就不会触发get拦截处理。
重点注意:在vue3中:只有三种类型的依赖即effect实例:computedEffect,watchEffect和renderEffect。所以ref/reactive响应式数据只有在被computed,watch和template模板中被引用了,才能够收集到对应的依赖。
二,在computed和watch初始化过程中,如果引用了ref/reactive数据,那么这时候ref/reactive就能够收集到对应的computedEffect和watchEffect实例,我们分开来讲解:
- computed:我们定义了一个computed计算属性,getter内部引用了一个ref数据。然后在computed实例初始化时,内部会创建computedEffect,同时将这个effect实例存储到了computed自身的effect属性上。计算属性虽然被创建完成了,但是还没有被访问,所以就没有执行一次getter,也就不会触发ref数据的get,也就不能收集到computedEffect。而计算属性被访问有两种情况:1.在后续的函数调用中被访问【也包括watch的回调函数调用,watch的回调函数中可以使用计算属性值】,2.在组件渲染时的模板中访问。假如以前面的情况为例,这时候计算属性被访问了,就会执行一次effect.run(),在run函数中就会将当前的computedEffect实例赋值给activeEffect。这时候activeEffect就有值了,然后run函数中还会调用fn回调函数,在这里就是getter。所以也就会触发ref的get拦截操作,ref数据收集依赖的两个变量条件都已经满足了,就能够继续触发后续的收集逻辑。【也就是收集这个computedEffect实例】
# 案例
const str = ref({name: 1});
const countTxt = computed(()=> {
return str.value.name + 'ms'
})
function fn() {
console.log(countTxt.value)
}
// 调用fn
// 1, 首先触发countTxt实例的get,countTxt收集自身computedEffect
// 2, computed调用getter,就会触发str的get,str就能够收集到computedEffect
fn()
- watch:我们定义一个watch,监听一个ref数据。在watch初始化过程中,内部也会创建一个watchEffect实例,同时在最后的初始化过程中会执行一个initial run,在这里会执行effect.run()【effect的run方法每次调用都会将自身实例赋值给activeEffect】,将watchEffect赋值给activeEffect,这时候的activeEffect就已经是我们需要的effect依赖了,然后执行Fn即getter就会触发ref的get,所以这时候ref数据就可以收集到正确的依赖了。
# 案例
const str = ref({name: 1});
const countTxt = computed(()=> {
return str.value.name + 'ms'
})
function fns() {
console.log(countTxt.value)
}
fns()
// 初始化最后,执行getter 即()=>str.value.name,就会触发ref数据的get,即可收集到watchEffect依赖
watch(()=>str.value.name, (val)=> {
console.log(val)
})
// 【dep是一个set结构,可以对内容自动去重,不会存在重复收集】
注意:computed实例和ref实例一样,也是响应式数据,也可以收集相关的effect依赖实例。同理如果watch或者watchEffect中引用了computed变量,那么这个computed变量也可以收集到watchEffect依赖实例。
下面我们继续收集逻辑,继续查看trackEffects源码:
function trackEffects(dep: Dep) {
# 默认不收集
let shouldTrack = false
// 当:当前递归追踪的层级小于JS引擎最大递归追踪层级时,可以继续追踪
if (effectTrackDepth <= maxMarkerBits) {
if (!newTracked(dep)) {
dep.n |= trackOpBit // set newly tracked
shouldTrack = !wasTracked(dep)
}
} else {
// Full cleanup mode.
// 清理所有追踪
shouldTrack = !dep.has(activeEffect!)
}
# 可以收集的情况下:
if (shouldTrack) {
// 默认的dep = Set(0) { w: 0, n: 0 },dep是一个set集合实例,这里用作收集依赖
# dep收集effect依赖实例
dep.add(activeEffect!)
# 同时,effect实例又将dep添加到自己的deps属性中【和vue2一样,互相收集】
activeEffect!.deps.push(dep)
}
}
根据上面的代码我们可以看出,除去前面的边界情况代码,trackEffects函数的作用就是向ref.dep属性添加当前的effect实例,同时又把自身dep实例添加到activeEffect.deps属性中,这和vue2的思想是一致的。
(二)触发依赖
// 触发ref的依赖
function triggerRefValue(ref: RefBase<any>, newVal?: any) {
ref = toRaw(ref)
if (ref.dep) {
// 触发依赖
triggerEffects(ref.dep)
}
}
继续查看triggerEffects源码:
function triggerEffects(dep: Dep | ReactiveEffect[]) {
# 如果是数组直接使用,如果不是:则为set实例,扩展set中的元素到数组
// [...dep]=[...Set(3)]:是set集合中的元素,展开为数组[effect, effect, effect]
const effects = isArray(dep) ? dep : [...dep]
// 循环依赖列表:先触发计算属性的依赖,是为了更新计算属性的值,从而让后续的依赖如果引用了计算属性能够获取到最新的值
for (const effect of effects) {
# 触发为计算属性的依赖effect
if (effect.computed) {
triggerEffect(effect, debuggerEventExtraInfo)
}
}
for (const effect of effects) {
# 触发其他依赖effect
if (!effect.computed) {
triggerEffect(effect, debuggerEventExtraInfo)
}
}
}
根据上面的源码可以看出:triggerEffects首先做了一个数据的格式处理,然后就是循环触发不同类型的effect实例,而triggerEffect函数就是具体执行effect实例的方法。
继续查看triggerEffect源码:
# 具体执行effect
function triggerEffect(effect: ReactiveEffect) {
if (effect !== activeEffect || effect.allowRecurse) {
if (effect.scheduler) {
# 存在调度程序的情况下;优先执行effect的调度任务,添加job到异步队列
// vue3的更新主要都是走调度程序
effect.scheduler()
} else {
// 或者直接run方法,调用fn
effect.run()
}
}
}
到这里我们也可以发现,触发依赖的最终目标就是执行effect实例的调度任务scheduler或者回调函数fn。
具体的调度任务和回调函数执行我们这里暂时不展开,后面异步更新队列会详细讲解。这里主要是分析ref数据的依赖收集与触发。
二,reactive
这节我们继续解析reactive响应式数据的收集依赖 和 触发依赖逻辑:
在分析之前,我们先看两个类型定义:
// 收集类型:查询
export const enum TrackOpTypes {
GET = 'get',
HAS = 'has',
// 迭代
ITERATE = 'iterate'
}
// 触发类型:增删改
export const enum TriggerOpTypes {
SET = 'set',
ADD = 'add',
DELETE = 'delete',
CLEAR = 'clear'
}
在这里我们可以发现,reactive响应式数据收集依赖有三种类型【GET,HAS,ITERATE】,触发依赖有四种类型【SET, ADD, DELETE, CLEAR】。不同的操作类型对应的不同的依赖操作逻辑,这也是体现了reactive创建的响应式数据:即proxy代理对象能够对目标对象实现完全数据拦截的强大功能,这也是Vue3的响应式核心采用ES6 Proxy的原因。
注意:ref数据只有get/set,因为ref响应式数据本质就只是对ref.value属性的操作。而一旦使用ref创建的响应式数据传入了集合类型,底层就会被reactive托管,所以说reactive是vue3响应式功能的底层核心。
(一)收集依赖
get {
track(target, TrackOpTypes.GET, key)
}
has {
track(target, TrackOpTypes.HAS, key)
}
# 追踪收集
function track(target: object, type: TrackOpTypes, key: unknown) {
// 这两个判断条件还是和之前一样
if (shouldTrack && activeEffect) {
# 从targetMap结构中 查询target对应的值
let depsMap = targetMap.get(target)
if (!depsMap) {
// 如果depsMap不存在,则添加一个新的键值对
targetMap.set(target, (depsMap = new Map()))
}
# depsMap存在:则从中取出key对应的dep实例
let dep = depsMap.get(key)
if (!dep) {
// 如果dep实例不存在,则向depsMap添加一个新的键值对
depsMap.set(key, (dep = createDep()))
}
# dep实例存在,收集依赖
trackEffects(dep, eventInfo)
}
}
这里我们要先看一个targetMap的定义:
# type KeyToDepMap = Map<any, Dep>
const targetMap = new WeakMap<any, KeyToDepMap>()
targetMap是一个weakMap结构,它的键为target目标对象,它的值为KeyToDepMap。同时KeyToDepMap自身也是一个map结构,它的键为属性key,它的值为dep实例。
理解了这些我们才可以继续向下分析:
在收集依赖时,需要先从targetMap里面查询target对应的值depsMap。
如果depsMap不存在,就向targetMap里面添加一个新的键值对:
# 键为 target 值为depsMap:一个空的map
targetMap.set(target, (depsMap = new Map()))
如果depsMap存在,则从depsMap结构中取出属性Key对应的dep实例:
# 如果depsMap存在:则从中取出key对应的dep实例
let dep = depsMap.get(key)
继续向下解析:
如果dep实例不存在,则向depsMap结构中添加一个新的键值对:
# 初始化一个dep实例
depsMap.set(key, (dep = createDep()))
如果dep实例存在,则开始执行依赖收集:
trackEffects(dep, eventInfo)
最后执行trackEffects方法,这个方法在ref里面已经解析,这里就不在重复。
注意:get / has / iterate三个收集依赖逻辑是一样的,区分主要是为了方便DEV环境调试,而触发依赖的逻辑是不同的,需要严格区分触发类型。
(二)触发依赖
set {
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
},
add {
trigger(target, TriggerOpTypes.ADD, key, value)
},
delete {
trigger(target, TriggerOpTypes.DELETE, key)
},
clear {
trigger(target, TriggerOpTypes.CLEAR)
}
# 触发依赖
function trigger(
target: object,
type: TriggerOpTypes,
key?: unknown,
newValue?: unknown,
oldValue?: unknown,
oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
# 从targetMap结构中 查询target对应的值
const depsMap = targetMap.get(target)
if (!depsMap) {
// 不存在depsMap,直接return,
return
}
# depsMap存在的情况下:
// 新建一个空数组:用于存储需要触发的dep实例
let deps: (Dep | undefined)[] = []
# 重点:区分不同的触发类型
if (type === TriggerOpTypes.CLEAR) {
# clear清空类型
// 展开所有dep实例, 需要触发target所有依赖
deps = [...depsMap.values()]
} else if (key === 'length' && isArray(target)) {
# 修改数组的length属性的情况下:
// 保留新的length
const newLength = Number(newValue)
depsMap.forEach((dep, key) => {
// 循环depsMap结构:将符合条件的dep实例添加到deps数组
if (key === 'length' || key >= newLength) {
deps.push(dep)
}
})
} else {
// schedule runs for SET | ADD | DELETE
# 其他类型触发情况:SET | ADD | DELETE
// void 0 代替 undefined:void 0无论什么时候都是返回undefined
if (key !== void 0) {
// 常规对象的key触发都在这里处理:
# 如果存在key,则从depsMap中取出key对应的dep实例,添加到deps数组
deps.push(depsMap.get(key))
}
// also run for iteration key on ADD | DELETE | Map.SET
# 兼容迭代结构处理:ADD | DELETE | Map.SET
switch (type) {
case TriggerOpTypes.ADD:
// 对象新增:处理迭代的情况
if (!isArray(target)) {
deps.push(depsMap.get(ITERATE_KEY))
if (isMap(target)) {
deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
}
} else if (isIntegerKey(key)) {
// new index added to array -> length changes
# 数组通过索引新增元素的情况:会触发length属性的变化,
// 取出length属性对应的dep实例添加到deps数组
deps.push(depsMap.get('length'))
}
break
case TriggerOpTypes.DELETE:
if (!isArray(target)) {
deps.push(depsMap.get(ITERATE_KEY))
if (isMap(target)) {
deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
}
}
break
case TriggerOpTypes.SET:
// 兼容map结构迭代
if (isMap(target)) {
deps.push(depsMap.get(ITERATE_KEY))
}
break
}
}
# 触发之前,判断deps有没有数据
if (deps.length === 1) {
if (deps[0]) {
// 常规情况下:都是走的这里
triggerEffects(deps[0])
}
} else {
// 创建一个effects数组
const effects: ReactiveEffect[] = []
for (const dep of deps) {
if (dep) {
// 取出deps中的deps实例,添加到effects
effects.push(...dep)
}
}
# 触发依赖
triggerEffects(createDep(effects))
}
}
根据上面的源码可以看出:trigger函数针对不同的数据类型,不同的触发类型都做了对应的兼容处理,保证任何操作都能正确的触发响应式数据的副作用,在对数据处理之后,依然是调用了triggerEffects方法来执行具体的触发逻辑。
vue3的响应式就分析到这里了,下节我们继续分析vue3的异步更新队列。
转载自:https://juejin.cn/post/7202801134556479549