
本文使用的
vue版本为3.2.26。在阅读effect源码之前,我们需要知道它的特性,可以通过阅读单例测试源码或者是阅读官网的API了解特性,推荐阅读单例,了解特性在后面阅读时才能更好理解。
通过上一章vue3-reactive源码解析,可以猜想到,effect主要职责是存储Proxy track (收集)的依赖,当Proxy triggle (触发)后查看trigger是否是track存储的依赖,如果是的话则执行监听函数。关于Proxy是如何track和triggle的可以看上一章vue3-reactive源码解析。
为了方便表达,我将用户传入effect的回调函数统称为监听函数,由effect包裹后的统称为收集函数。
通过文档和单例可以知道effect有以下特性
effect内触发了已经收集的依赖。比如effect(() => {rea.a; rea.a = 2})effect函数返回的也是函数,可以直接通过返回的函数执行监听函数。effect可以包裹effect返回的方法,会重新包装,当触发时会执行两次。effect可以在监听函数中再次调用effect,但是里层不会收集外层监听函数的依赖,外层也不会收集到里层的依赖,比如: const rea = reactive({a: 1, b: 2})
// 1, 2
effect(() => {
console.log(rea.a)
effect(() => {
console.log(rea.b)
})
})
// 2, 2
rea.a = 2
// 3
rea.b = 3
effect第二个可选tson参数,这个参数包含lazy, scheduler, allowRecurse, onStop, onTrack, onTragger属性lazy:boolean,是否懒加载,如果是true调用effect不会立即执行监听函数,由用户手动触发。scheduler: function,被触发引起effect要重新收集依赖时的调度器,当传入时effect收到触发时不会重新执行监听函数而是执行这个function,由用户自己调度。allowRecurse:是否允许递归,这个参数需要和scheduler配套使用,是否允许递归scheduler,对监听函数无效。onStop:当effect被stop (停止监听)时的钩子。onTrack:当effect被track时的钩子。onTrigger:当effect被trigger时的钩子。如果是我们自己编写effect会怎么实现呢?上一章知道了Proxy会通过track函数告知我们收集到了哪个对象的哪个key,会通过triggle函数因为哪个对象的哪个key引起了触发。加上上面的阅读准备我们知道了effect大概需求
track只收集effect内的依赖,trigger是触发effect的收集函数或调度器。effect收集函数被重新执行时需要清空之前收集的依赖并重新收集,因为收集的可能存在分支比如if,收集也是动态的。到这里是不是有大概的思路了,根据需我们可以简单的实现大概逻辑:
在vue中effect实现的原理流程其实跟上图是差不多的,接下来我们就一份一份拆解出来看看它各个部分是怎么实现的,我们先看看target,key,effect的关系在里面是怎么实现的。
vue中是如何存储target,key,effect的关系的,在effect源码中我们可以看到顶部有一段代码
// Dep: Set对象,可以存储多个effect对象
export type Dep = Set<ReactiveEffect> & TrackedMarkers
// Dep附加对象,用来标识effect的状态
type TrackedMarkers = {
/**
* wasTracked
*/
// 之前被收集
w: number
/**
* newTracked
*/
// 当前被收集
n: number
}
// key关联Dep的Map 为了方便我们叫他kDepMap
type KeyToDepMap = Map<any, Dep>
// Target -> kDepMap
const targetMap = new WeakMap<any, KeyToDepMap>()
在effect源码文件中声明了一个类型为WeakMap的targetMap变量,这个targetMap就是存储监听函数执行期间Proxy中track出来的依赖与effect。
Proxy调用track时抛出的参数中有代理的Target(raw)和引起track的key,而一个Target可以有多个key引起track,key也可能是对象,因为Target可能是Map或者WeakMap,除了存储Target和key外,也要存储这个key是在哪些effect的监听函数中使用的,所以vue采用双Map的存储方式。kDepMap存储每个key和effect的引用关系,然后targetMap存储target和kDepMap的引用关系。
上面还使用了ts为Dep的类型来标记kDepMap的value,Dep在Set的基础上附加值为number属性w、h。Dep中的w和n是干什么用的呢,我们看到源码中的注释wasTracked和newTracked从字面意思可以猜测出来,应该是记录之前是否被收集和现在是否被收集。我们之前讲过,数据收集是动态的,所以每次执行收集前需要清空之前的依赖,然后附加上现在的依赖,确保依赖正确,比如下方的代码:
const reuser = reactive({
name: 'bill',
sex: '男',
setLog: 'name'
});
// 第一次执行收集到的key是setLog、name
effect(() => {
console.log(ret[reuser.setLog])
})
// 更改后收集到的key是setLog、sex
reuser.setLog = 'sex'
vue中将这两个属性直接关联到Dep中,也就是说Target的每个key都有当前之前是否被收集、现在是否被收集的标识状态。按照平常的做法来说这种状态应该是附加到effect实例,因为Dep是Set它里面存储的不止是一个effect,每个effect都应该有状态,但是现在附加到了Dep上,也就意味着必须要对Dep的w、n做一些特殊的处理:
effect函数执行完毕之后必须要还原Dep的w和h的状态,否则Set中其他effect使用就不正确了effect函数执行前必须恢复w属性(之前是否被trick)effect函数递归调用时,w和h属性必须能够完整记录每一层的状态,比如下方这种方式调用 const rea = reactive({ a: 1, b: 2 })
effect(() => {
console.log(rea.a);
rea.a = 2
effect(() => {
console.log(rea.a)
})
})
因为这些都是与effect中实现直接挂钩的,等我们讲到effect具体实现时再看看他们是怎么具体实现的。现在我们可以先思考w、h怎么实现这三点的,前两点都还好,只是恢复和还原状态,但是第三点要记录多层状态。要记录多层状态,而w、h又是number,可以得出结论,这两个属性是要用位运算符做多层状态管理,关于位运算符是怎么做状态的,大家可以看看我之前写的这篇文章。递归调用effect时每一层effect,w、h都用一个特定的位来标识这一次effect的状态,在二进制中的用1表示true,0表示false这是常规做法。在vue中使用的是从第二位开始标记当前状态,每多一层就将当前标识状态的往前推一位,例如:
const rea = reactive({ a: 1 })
//初次生成
// key: a
// Dep: { w: 0, n: 0 }
// w.toString(2): 00000000000000000000000000000000
// n.toString(2): 00000000000000000000000000000000
//
effect(() => {
//第一次收集
// key: a
// Dep: { w: 0, n: 2 }
// w.toString(2): 00000000000000000000000000000000
// n.toString(2): 00000000000000000000000000000010
//第二次收集,恢复已经被收集状态
// key: a
// Dep: { w: 2, n: 2 }
// w.toString(2): 00000000000000000000000000000010
// n.toString(2): 00000000000000000000000000000010
//
console.log(rea.a);
effect(() => {
// 进入内层
// 第一次收集
// key: a
// Dep: { w: 0, n: 6 }
// w.toString(2): 00000000000000000000000000000000
// n.toString(2): 00000000000000000000000000000110
// 第二次收集,恢复已经被收集状态
// key: a
// Dep: { w: 6, n: 6 }
// w.toString(2): 00000000000000000000000000000110
// n.toString(2): 00000000000000000000000000000110
//
console.log(rea.a)
//离开外层清空恢复进入状态
//第一次收集离开
// key: a
// Dep: { w: 0, n: 2 }
// w.toString(2): 00000000000000000000000000000000
// n.toString(2): 00000000000000000000000000000010
//第二次收集离开
// key: a
// Dep: { w: 2, n: 2 }
// w.toString(2): 00000000000000000000000000000010
// n.toString(2): 00000000000000000000000000000010
//
})
//离开外层清空恢复进入状态
// key: a
// Dep: { w: 0, n: 0 }
// w.toString(2): 00000000000000000000000000000000
// n.toString(2): 00000000000000000000000000000000
//
})
rea.a = 2
要标识当前递归调用了多少次,还需要用一个变量来记录,在effect中使用effectTrackDepth变量来记录,现在来看看Dep的具体管理方法:
// -----effect文件中------
// 当前effect层叠数
let effectTrackDepth = 0
// 当前trick需要操作的bit
export let trackOpBit = 1
// 在使用时 trackOpBit = 1 << ++effectTrackDepth
// 也就是effect递归多少次就往前推多少位
// effectTrackDepth = 0 trackOpBit = 00000000000000000000000000000010
// effectTrackDepth = 1 trackOpBit = 00000000000000000000000000000100
// effectTrackDepth = 2 trackOpBit = 00000000000000000000000000001000
// effectTrackDepth = 3 trackOpBit = 00000000000000000000000000010000
// ----------------------
// 创建dep
export const createDep = (effects?: ReactiveEffect[]): Dep => {
const dep = new Set<ReactiveEffect>(effects) as Dep
// 初始化
dep.w = 0
dep.n = 0
return dep
}
// 传入的Dep 查看当关联key在effect中之前是否被track
export const wasTracked = (dep: Dep): boolean => (dep.w & trackOpBit) > 0
// 传入的Dep 查看当关联key在effect中现在是否被track
export const newTracked = (dep: Dep): boolean => (dep.n & trackOpBit) > 0
为了方便操作vue会创建一个trackOpBit变量,这个变量根据当前effect的递归往前推进,保证trackOpBit的二进制位数中为1的位置和w、h二进制数标识当前effect状态的位置是保持一致的。当需要判断key在当前effect之前和现在是否被收集时只需要dep.w & trackOpBit和dep.n & trackOpBit是否大于0就行了,如果对于 &运算符不了解可以看看我之前写的这篇文章。
通过Dep记录的这某个key上一次是否被收集和现在是否被收集,我们可以猜测到vue是怎么管理targetMap的了,vue中重新收集时 (即调用effect监听函数)可能不是简单粗暴的直接剔除KeyToDepMap中Set所有当前的effect,然后再收集,而是:
key被收集,但是当前没有收集,则在key关联的Dep中剔除当前effectkey没有被收集,当时当前被收集,则在key关联的Dep中添加当前effect`key被收集,当前也被收集,则保持不变 接下来我们看看effect函数的具体代码
export const extend = Object.assign
// 创建effect函数
export function effect<T = any>(
fn: () => T,
options?: ReactiveEffectOptions
): ReactiveEffectRunner {
// 如果当前fn已经是收集函数包装后的函数,则获取监听函数当做入参
if ((fn as ReactiveEffectRunner).effect) {
fn = (fn as ReactiveEffectRunner).effect.fn
}
// 创建effect对象
const _effect = new ReactiveEffect(fn)
// 将用户传入的参数附加到effect对象上
if (options) {
extend(_effect, options)
// 如果有定义域作用于则记录,这个我们后面章节再讲,这里不影响主流程
if (options.scope) recordEffectScope(_effect, options.scope)
}
// 如果不是懒加载则立即执行包装后的监听函数
if (!options || !options.lazy) {
_effect.run()
}
// 绑定收集函数的this对象,和effect对象
const runner = _effect.run.bind(_effect) as ReactiveEffectRunner
runner.effect = _effect
return runner
}
effect函数主要是创建ReactiveEffect对象,将用户传入的参数附加到对象上,履行lazy参数的职责。
effect返回的是effect.run函数,这个函数的effect属性会指向effect对象,this也会设置为effect对象。所以当懒加载时,或者用户主动执行effect包装后的监听函数,也能够正确的track。
我们看到入参时会查看监听函数是否是effect包装后的函数,如果是会拿到未包装前的监听函数 (存储再effect对象的fn属性上)再创建effect,所以**effect可以包裹effect返回的方法,会重新包装,当触发时会执行两次。**
这里ReactiveEffect采用了class写法,每个effect函数都会创建一个实例,接下来我们看看这个class的具体代码
// 最多30个互相引用,如果超出则清理
const maxMarkerBits = 30
// 正在执行的effect栈
const effectStack: ReactiveEffect[] = []
// 当前正在执行的effect
let activeEffect: ReactiveEffect | undefined
let effectTrackDepth = 0
// effect对象
export class ReactiveEffect<T = any> {
// 当前对象是否是有效的,为false则是已加stop的了
active = true
// 记录当前effect 收集到的所有key对应的Dep
deps: Dep[] = []
// 是否是computed 创建后可以附加
computed?: boolean
// 是否允许递归响应
allowRecurse?: boolean
// 停止监听钩子
onStop?: () => void
// 被收集时钩子
onTrack?: (event: DebuggerEvent) => void
// 被触发时钩子
onTrigger?: (event: DebuggerEvent) => void
// 构造函数
constructor(
// 监听函数
public fn: () => T,
// 调度器
public scheduler: EffectScheduler | null = null,
// 作用域
scope?: EffectScope | null
) {
// 记录当前对象的空间范围
recordEffectScope(this, scope)
}
// 收集函数
run() {
// 如果当前effect已经被stop
if (!this.active) {
// 直接监听函数,不做收集逻辑
return this.fn()
}
// 查看当前调度栈是否包含当前对象,如果包含说明是嵌套运行,不再执行
if (!effectStack.includes(this)) {
try {
// 当前effect入栈
effectStack.push((activeEffect = this))
// 开启收集
enableTracking()
// 根据层叠数更改trackOpBit
trackOpBit = 1 << ++effectTrackDepth
// 查看当前effect层叠数是否超过允许的最大记录数
if (effectTrackDepth <= maxMarkerBits) {
// 记录恢复上一次dep状态 也就是更改w
initDepMarkers(this)
} else {
// 如果超过了最大bit记录数,则清除当前effect关联的所有Dep映射
cleanupEffect(this)
}
return this.fn()
} finally {
// 如果当前effect轮询个数没超限制
if (effectTrackDepth <= maxMarkerBits) {
// 整理effect deps 删除失效无用的dep, 恢复 dep w n状态
finalizeDepMarkers(this)
}
// 恢复执行位数
trackOpBit = 1 << --effectTrackDepth
// 恢复收集状态
resetTracking()
// 出栈
effectStack.pop()
// 将正在使用effect替换成栈顶
const n = effectStack.length
activeEffect = n > 0 ? effectStack[n - 1] : undefined
}
}
}
// 停止监听
stop() {
if (this.active) {
// 清除当前effect关联的所有Dep映射
cleanupEffect(this)
if (this.onStop) {
this.onStop()
}
this.active = false
}
}
}
关于computed、scope和recordEffectScope我们后面的章节再讲,这里不会影响当前业务,先忽略它们。
effect函数传入的参数都会附加到ReactiveEffect对象上,其中scheduler可以通过构造函数传入。effect对象上还会附加deps属性,这个属性是记录effect关联的所有key的Dep对象。这里附加是因为响应式对象不止只有reactive, 还有其他响应式对象的依赖需要存储,其他响应式对象我们后面再讲。还有一方面是为了方便的管理,比如在执行前会还原当前Dep在之前是否被收集,执行完毕后需要对当前关联的所有Dep还原状态,停止监听时直接通过关联的dep删除effect。
在effect对象中使用run方法执行监听方法和附加状态。当effect对象被停用时调用run方法只是执行监听方法。
当进入收集函数时会进行检测当前对象是否已经在执行栈内,如果在栈内则中断执行,我们可以看到allowRecurse参数并没有在这里使用上,所以即使声明了allowRecurse参数对于收集函数的递归也是没什么效果。
正式进入会将正在执行的effect对象替换成当前effect对象,并且入栈。当在一个收集函数内调用另一个收集函数时时会叠加effectTrackDepth变量。还有我们之前说的trackOpBit变量,确保trackOpBit的中1的位数是跟Dep的w、h标识当前effect的位置是一致的,这样就能正确的使用wasTracked和newTracked方法。
收集函数还有个最大叠层数限制这个层叠数是30,在maxMarkerBits中声明。在ts中number中使用32位的二进制数来表示数字的,第32位是符号位(0为正,1为负),那就是说最多能表示31个状态,而在w、n中最后一位没有使用,第一次进去是使用1 << 1,直接从第二位开始的。所以最大的层数只能是30。
执行收集函数时是怎么恢复上次是否被收集的状态呢,因为每个effect对象都记录了key关联上的dep( *deps*),当最新进入时,这些Dep就是上次的收集值,如果当前层叠数没超过30次,只需要在最新执行前将这些dep都打上之前被收集的标记就行了,在收集函数中使用initDepMarkers函数来实现的,下面我们看看源码
// 初始effect dep 的 记录
export const initDepMarkers = ({ deps }: ReactiveEffect) => {
if (deps.length) {
for (let i = 0; i < deps.length; i++) {
deps[i].w |= trackOpBit // set was tracked
}
}
}
如果层叠数超过30次呢?这时候w,h无法正确的记录状态了,要怎么正确的收集和更新dep呢?因为无法记录状态,所以不知道之前是否收集过,那么就执行简单粗暴的方法,直接将之前effect对象收集的Dep删除掉,并删掉Dep中effect对象的引用,那新增加的就一定是正确的,这样就绕过需要状态的问题了。在effect是通过cleanupEffect方法清空当前effect对象与Dep的互相引用的,我们看看实现这个方法的实现
// 清空当前effect对象与Dep的互相引用的
function cleanupEffect(effect: ReactiveEffect) {
const { deps } = effect
// 清除Dep中effect的引用
if (deps.length) {
for (let i = 0; i < deps.length; i++) {
deps[i].delete(effect)
}
deps.length = 0
}
}
到这我们就看完了执行前的准备就已经完成,现在看看执行后effect的deps和Dep对象的处理是怎么处理的。
如果已经超过最大层叠数,则effect的deps和Dep不需要做任何处理,因为之前收集的Dep已经被删除了,现在存下来的肯定是最新,而且也没用到Dep的w和n状态。
如果没超过最大层叠数,Dep的w和n因为是被多个effect对象引用,所以执行后要恢复到进入时的状态,确保其他effect对象使用时是正确的。为什么不能直接还原到最初的状态 *({w: 0, n: 0})*,因为收集函数可能互相引入,当前收集函数执行完,执行权还要交还给上一个收集函数,要确保上一个收集函数内的w、h状态正确。除了恢复状态我们还要更新effect对象的deps属性,在执行前都打上了被收集的标识,那么执行后只需要查看key关联的Dep现在是否被收集就能判断是需要删除或保留 (添加是在trick方法进行的)。在vue中是使用finalizeDepMarkers函数来管理这部分需求的,接下来我们看看实现:
// 更新effect对象的deps属性
export const finalizeDepMarkers = (effect: ReactiveEffect) => {
// 获取deps
const { deps } = effect
if (deps.length) {
let ptr = 0
for (let i = 0; i < deps.length; i++) {
const dep = deps[i]
// 如果之前dep已经收集,但是当前没有被收集,直接删除
if (wasTracked(dep) && !newTracked(dep)) {
dep.delete(effect)
} else {
// 更新deps
deps[ptr++] = dep
}
// clear bits
// 清除dep在这次收集函数中的状态
dep.w &= ~trackOpBit
dep.n &= ~trackOpBit
}
// 更新deps
deps.length = ptr
}
}
如果之前收集过但是现在没收集的则直接删除,否则就保留,并且每个dep中标识当前effect状态的位标识符都重置为进入时的状态。这个函数通过记录当前保留的总数,然后要删除的dep的位置替换成要保留的dep,最后更新length,写的也是相当精妙。
在恢复dep状态和更新effect对象的deps后,也会将当前trackOpBit、activeEffect、effectStack恢复到进入前状态。到这里收集方法就已经看完了,接下来我们看看实例上的另外一个方法stop,这个方法相对比较简单,只是清空targetMap中当前effect对象引用,调用停用钩子,并更改当前effect对象的状态 (active)为已经被停用。effect中还为这个方法对外提供了一个主动调起的辅助方法stop:
export function stop(runner: ReactiveEffectRunner) {
runner.effect.stop()
}
到这里effect对象里面具体的实现已经讲完了,那track又是怎么存储effect到Dep的,又是怎么将effect和Dep两者关联的呢,接下来让我们探索track里的具体实现。
让我们回顾一下之前内容,track函数是在Proxy的基础拦截器或者是集合修改器中获取数据时触发的,主要是关联effect跟收集到的依赖,接下来我们看看track函数的具体实现,先看看具体代码
// 当前是否正在收集,当前开启收集,并且有正在使用的effect对象
export function isTracking() {
return shouldTrack && activeEffect !== undefined
}
// 收集effect对象的依赖建立关系
export function track(target: object, type: TrackOpTypes, key: unknown) {
// 如果当前没有进行收集则直接返回
if (!isTracking()) {
return
}
// 获取 KeyToDepMap(keys -> Dep)
let depsMap = targetMap.get(target)
// 如果不存在则初始化KeyToDepMap
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
// 获取当前 key的Dep
let dep = depsMap.get(key)
// 如果不存在则创建Dep
if (!dep) {
depsMap.set(key, (dep = createDep()))
}
// 如果是开发环境则记录具体信息
const eventInfo = __DEV__
? { effect: activeEffect, target, type, key }
: undefined
trackEffects(dep, eventInfo)
}
track函数的逻辑并不复杂,首先检测当前是否正在收集,这个判断就是当前trackStack栈顶是开启了收集,并且当前正在执行收集函数,如果不是正在收集则直接退出,确保只有正在执行收集函数时才能进入。然后查看映射关系中是否存在当前Target的KeyToDepMap(keys -> Dep)如果不存在则创建,在通过key查找是否有Dep没有则创建。如果是开发环境还会创建需要附加到钩子的具体收集信息,最后调用trackEffects方法,可以猜测得到trackEffects才是真正实现具体业务的方法。
下面我们看看trackEffects的具体实现代码:
// track,更改Dep状态,更新effect对象的deps
export function trackEffects(
dep: Dep,
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
// 是否是新增的依赖
let shouldTrack = false
// 查看当前层叠数是否能超过了记录的最大限制
if (effectTrackDepth <= maxMarkerBits) {
// 查看是否记录过当前依赖
if (!newTracked(dep)) {
// 记录是当前收集的依赖
dep.n |= trackOpBit // set newly tracked
// 如果effect之前已经收集过,则不是新增依赖
shouldTrack = !wasTracked(dep)
}
} else {
// 如果层叠数超过了最大,则查看当前dep在effect中实收存储过
// 因为超过最大进入前会清空所有dep,
// 第一次进入一定会收集,当收集重复key时才会跳过
shouldTrack = !dep.has(activeEffect!)
}
// 如果是新增的收集
if (shouldTrack) {
// dep添加当前正在使用的effect
dep.add(activeEffect!)
// effect的deps也记录当前dep 双向引用
activeEffect!.deps.push(dep)
// 如果当前是开发环境,还要执行onTrack钩子
if (__DEV__ && activeEffect!.onTrack) {
activeEffect!.onTrack(
Object.assign(
{
effect: activeEffect!
},
debuggerEventExtraInfo
)
)
}
}
}
没超过最大层叠数时,收集函数收集到的Dep需要打上当前被收集的状态,给effect对象执行完毕后更新deps属性使用性。如果当前收集到了dep,但是之前不存在,说明这个dep是新增的。当超过最大层叠数时执行前就清空之前的所有Dep中当前effect对象的引用,所以当进入收集函数时所有dep就都是新增的。新增的dep时需要将当前effect添加到这个Dep中,并且将这个dep添加到当前effect的deps中,然后触发收集钩子。
到这里track函数里面具体的实现已经讲完了,effect通过监听函数执行前设置当前effect,并使用Dep的w和n属性标记状态,然后在track中使用,通过这种方式确定当前是否在effect内收集到的依赖,确定状态,更新状态。
接下来我们看看triggle函数是如何通过target,key和targetMap存储库确定要执行的收集函数。
// trigger 值变化
export function trigger(
target: object,
type: TriggerOpTypes,
key?: unknown,
newValue?: unknown,
oldValue?: unknown,
oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
// 获取当前target的KeyToDepMap
const depsMap = targetMap.get(target)
if (!depsMap) {
// never been tracked
return
}
// 需要触发的deps
let deps: (Dep | undefined)[] = []
if (type === TriggerOpTypes.CLEAR) {
// 如果是清除当前数据(Set和Map中的操作),那所有dep都应该触发
deps = [...depsMap.values()]
} else if (key === 'length' && isArray(target)) {
// 如果是修改数组长度,
// length和被删除的下标的key 关联的dep都应该被触发
depsMap.forEach((dep, key) => {
if (key === 'length' || key >= (newValue as number)) {
deps.push(dep)
}
})
} else {
// 先获取当前key关联的deps
if (key !== void 0) {
deps.push(depsMap.get(key))
}
// also run for iteration key on ADD | DELETE | Map.SET
// 判别操作类型,
// 有些操作会关联到其他操作,需要分别判断
switch (type) {
// 如果是增加操作
case TriggerOpTypes.ADD:
// 数组需要单独判断,之前我们说过数组的迭代收集到的key是length
if (!isArray(target)) {
// 因为是新增,获取迭代收集的dep
deps.push(depsMap.get(ITERATE_KEY))
if (isMap(target)) {
// 如果是map还需要收集MAP_KEY_ITERATE_KEY
deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
}
} else if (isIntegerKey(key)) {
// 如果是数组新增下标那么length一定会修改
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))
}
}
// 因为删除操作一定会有length属性的变化,会引起length的triggle,这里就不需要重复收集
break
// 如果是更改
case TriggerOpTypes.SET:
// 用户可能直接获取map.values或者 map.entries直接拿到value
if (isMap(target)) {
deps.push(depsMap.get(ITERATE_KEY))
}
break
}
}
// 附加trigger调试信息给onTrigger钩子使用
const eventInfo = __DEV__
? { target, type, key, newValue, oldValue, oldTarget }
: undefined
// 假如只有一个Dep依赖则直接triggerEffects
if (deps.length === 1) {
if (deps[0]) {
if (__DEV__) {
triggerEffects(deps[0], eventInfo)
} else {
triggerEffects(deps[0])
}
}
} else {
// 假如有多个deps需要对内部的effect做一遍去重
const effects: ReactiveEffect[] = []
for (const dep of deps) {
if (dep) {
effects.push(...dep)
}
}
if (__DEV__) {
triggerEffects(createDep(effects), eventInfo)
} else {
triggerEffects(createDep(effects))
}
}
}
trigger不仅只是获取get在targetMap指定key的Dep,因为数据操作中有很多关联性的东西,比如新增和删除都需要重新触发迭代操作,下面我们详细分析各个操作的关联性。
Map执行clear时,需要触发所有之前收集的effectArray更新length时,之前收集到length值大于当前length值,那么存储库中之前收集到的下标小于等于当前length的key关联的Dep需要重新触发,因为有可能之前有值,现在值被删除;为什么是有可能呢因为当收集到超出边界的下标时更改length也会重复触发,例如: const rearr = reactive([1,1,1,1,1] as any[])
effect(() => {
console.log(rearr[4])
})
effect(() => {
console.log(rearr[6])
})
rearr.pop()
// 1 undefined
// undefined undefined
key为ITERATE_KEY、Target为Map中Key为MAP_KEY_ITERATE_KEY和Target为Arrat的key为length都应该触发,前两个很好理解,添加自然迭代就需要触发,但是第三个不是添加也会更改length吗,为什么也需要触发呢?这是因为当使用api时底层里面会先添加数据,这时数据内的length直接就被更改了,当拦截到length更改时已经获取不到旧值,前面我们看Proxy的set处理器触发前会做一条判断,那就是只有key的value更改了才会触发,这里length始终不会触发,因为始终是一致,所以当添加时就应该要触发。 const t = new Proxy([1,2,3,4], {
set(target, key, value) {
console.log(key, value, target[key])
target[key] = value
return true
}
})
t.push(4)
// 4 4 undefined
// length 5 5
t.splice(4, 0, 5)
// 5 4 undefined
// 4 5 4
// length 6 6
key为ITERATE_KEY、Target为Map中Key为MAP_KEY_ITERATE_KEY都应该触发,为什么这里就不需要触发Target为Arraykey为length的effect,这是因为底层删除数组某项时都是通过更改length来实现,能够获取到旧值,当length新旧值发生更改时能够trigger所以就不需要重复收集了。...
t.pop()
t.splice(3, 1)
// length 3 4
// length 3 3
Map中key为ITERATE_KEY都应该触发。为什么只要Map中的呢,因为如果是Array或者tson时,都得通过具体key来访问,在deps.push(depsMap.get(key))就能收集到;而Map可以通过entries和values直接获取,所以Map应该关联上ITERATE_KEY,而Set数据结构并没有提供直接修改的方法所以也不需要判断。 如果通过获取回来关联的Dep只有一个的话就直接触发里面所有的effect,如果是获取到多个Dep的话需要对effect去重,因为一个effect可能在一次触发中被收集多次,比如下方代码。代码中去重的方法是对所有Dep(Set)扩散,然后放入到一个新的Dep中而Dep是Set对象就会自动去重。
const name = { name: 'key' }
const remap = new Map([[name, 1]])
const te = effect(() => {
console.log(remap.get(name))
console.log([...remap.values()])
})
remap.set(name, 2)
// 直接获取到key为name的dep
// 获取执行remap.values方法获取到key位ITERATE_KEY的dep
// 两个dep都包含map,只需要执行一次,去重
到这里就已经获取到所有关联的effect了,然后传入到triggerEffects函数中,triggerEffects函数就是具体执行effect监听函数的实现,我们看看具体代码
// 执行因为trigger变化的所有effect
export function triggerEffects(
dep: Dep | ReactiveEffect[],
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
// 获取所有effect
for (const effect of isArray(dep) ? dep : [...dep]) {
// 如果触发关联的effect 是当前正在执行的,并且没有声明允许递归则不在重复执行
if (effect !== activeEffect || effect.allowRecurse) {
// 触发onTrigger钩子
if (__DEV__ && effect.onTrigger) {
effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))
}
// 如果当前effect有注册调度器,则使用调度器,否则则执行effect注册的函数
if (effect.scheduler) {
effect.scheduler()
} else {
effect.run()
}
}
}
}
triggerEffects传入参数允Array或Dep,会对所有的effect做一次遍历,逐一执行触发钩子、监听函数,大家注意到如果用户自定义了调度器scheduler的话是执行scheduler并不会直接执行监听函数。
当前effect是当前正在执行监听函数的effect时会有三种特殊情况:
allowRecurse为false则直接跳过allowRecurse为true没有自定义调度器时,将执行收集钩子和收集函数,但是执行监听函数前会判断当前effect是否在执行栈中,如果是直接跳过,所以这里只是执行了收集钩子,监听函数并没有允许递归allowRecurse为true有自定义调度器时,将执行钩子和自定义调度器,允许递归有效。 allowRecurse对于监听函数并没有实质作用,即使声明了也不会允许递归,它是作用于scheduler的。
effect执行收集函数时不会触发自身effect函数返回的是收集方法,可以显示调用effect函数可以传递effect函数返回的方法,会重新包装,但是源绑定方法是一致的effect监听函数中可以再调用其他收集函数,被调用者不会收集到当前effect的依赖effect可以在外层套一层effect继续监听effect可选参数{ lazy: 懒加载, scheduler: 调度器, scope: .., allowRecurse: 是否允许递归 , onStop: 停止调度钩子, onTrack: 收集时钩子, onTragger: 触发时钩子 }allowRecurse参数是针对scheduler的vue通过targetMap将effect和收集的target和key建立关系。key通过Dep与effect建立关系,effect通过缓存deps与key建立关系effect的deps管理方式有两种,effect层叠数少于30时通过w、n状态细粒增删,超过30则进入前删,后续都是增上一章:vue3-reactive源码解析
下一章:vue3-ref源码解析
©2021 - bill-lai 的小站 -站点源码