阅读准备
本文使用的
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
第二个可选json
参数,这个参数包含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
的关系在里面是怎么实现的。
Dep
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
中剔除当前effect
- 之前
key
没有被收集,当时当前被收集,则在
key关联的
Dep中添加当前
effect` - 之前
key
被收集,当前也被收集,则保持不变
effect
接下来我们看看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
中声明。在js
中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函数
让我们回顾一下之前内容,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 函数
接下来我们看看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
时,需要触发所有之前收集的effect -
当
Array
更新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
为Array
key为length
的effect
,这是因为底层删除数组某项时都是通过更改length
来实现,能够获取到旧值,当length
新旧值发生更改时能够trigger
所以就不需要重复收集了。... t.pop() t.splice(3, 1) // length 3 4 // length 3 3
-
当更改数据时,所有依赖收集到
Map
中key
为ITERATE_KEY
都应该触发。为什么只要Map
中的呢,因为如果是Array
或者json
时,都得通过具体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-ref源码解析