Vue3源码学习——副作用函数effect
上一节在介绍响应式原理的时候,一直有提到副作用函数effect,这一节,就深入的去了解一下effect。
如果之前有了解过 Vue2 源码的朋友,大概也能感觉到,Vue3 的响应式原理和Vue2其实差别上并不大。
- 在 Vue2 中,主要是通过 Object.defineProperty 来劫持对象属性,同时为每一个属性实例化一个用于收集 watcher 的容器dep,在 get 阶段,将该属性相关的 watcher 维护进 dep 容器中,在 set 阶段,则去调用dep中所收集到的每个watcher的update方法完成更新。
Vue2响应式原理图:

- 在Vue3中,劫持属性的方法更新为了Proxy,整体逻辑没有太大变化,依然是在get阶段收集依赖,在set阶段触发依赖,只不过这里原来在Vue2中,在每个属性的dep容器中收集到的watcher变成了副作用函数effect。
effect
这一节我们重点想了解effect,那我们就从源码中找一下effect出现的位置。
我们先跟着上一节的足迹,看一下trackEffects函数:
function trackEffects(
dep: Dep,
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
...
if (shouldTrack) {
// activeEffect 被收集进dep中
dep.add(activeEffect!)
activeEffect!.deps.push(dep)
...
}
}
在get阶段的 trackEffects 函数中,我们可以发现有一个 activeEffect 变量被收集进了 dep 中,我们大概就可以猜出这个 activeEffect 就是我们要找的 effect副作用函数,而这个activeEffect 又是在哪里被赋值的呢?
这里我们定位到了 ReactiveEffect类,在这个类的 run方法中会将当前类的实例this赋值给activeEffect,那么这里我们就基本可以确定ReactiveEffect类即为我们要找的创建effect副作用函数的类。
class ReactiveEffect<T = any> {
...
run() {
...
this.parent = activeEffect
activeEffect = this
...
}
ReactiveEffect类的内容较多,我们这里暂时先不看ReactiveEffect类的具体内容,先看看Vue3是在哪里创建ReactiveEffect实例的呢?
effect函数
function effect(fn,options) {
// 如果fn已经是effect函数了,则指向原来的函数
if (fn.effect) {
fn = fn.effect.fn
}
// 创建ReactiveEffect实例
const _effect = new ReactiveEffect(fn)
if (options) {
extend(_effect, options)
if (options.scope) recordEffectScope(_effect, options.scope)
}
// 如果没有options或者不是懒加载则执行_effect.run
if (!options || !options.lazy) {
_effect.run()
}
// 将函数执行的方法返回出去
const runner = _effect.run.bind(_effect)
runner.effect = _effect
return runner
}
这里我们定位到了effect函数,effect函数创建了 _effect 变量去接收创建出来的ReactiveEffect实例,在后面运行 _effect.run 方法,也就是我们上面提到的ReactiveEffect实例的run方法,将副作用函数effect维护进dep中。
关于effect这个API我们也可以在代码中进行尝试:
let obj = { count: 0 }
const state = reactive(obj)
effect(() => {
console.log(state.count)
})
这样我们每次修改state.count的时候,控制台都会打印state.count的值。而我们在上一节响应式原理中有提到,响应式的实现是在get阶段收集副作用函数,在set阶段去触发副作用函数的执行。那么也就可以说明,我们传入effect的 fn 参数里面的变量肯定是被访问过的,换句话说,按照上一节的理论,我们就可以猜测这个传入的fn,一定是被执行过了的,那这次我们就带着我们的这一猜测去ReactiveEffect类中找答案:
ReactiveEffect类
// 响应上下文中的嵌套层次数
let effectTrackDepth = 0
// 二进制位,每一位用于标识当前effect嵌套层级的依赖收集的启用状态
let trackOpBit = 1
// 标识最大标记的层级数
const maxMarkerBits = 30
// 当前激活状态的effect
let activeEffect
// 设置追踪功能是否打开,上一节我们有提到在使用一些数组方法的时候需要关闭追踪
let shouldTrack = true
class ReactiveEffect<T = any> {
// 用于标识副作用函数是否位于响应式上下文中被执行
active = true
// 用于收集副作用函数容器的数组
// 在trackEffect函数中会执行以下代码,从而将收集当前副作用函数的容器维护进deps中:
// ...
// dep.add(activeEffect!)
// activeEffect!.deps.push(dep)
deps: Dep[] = []
// 当effect发生嵌套的时候,指向上一层级的effect
parent: ReactiveEffect | undefined = undefined
// 省略一些暂时不关注的内容
...
run() {
// 若当前 ReactiveEffect 对象脱离响应式上下文,那么其对应的副作用函数被执行时不会再收集依赖
// (active默认是true,在stop方法中可以被赋值为false,而stop方法通常在卸载环节被触发,
// 不管是组件卸载还是监听器卸载,副作用函数当然不再需要被收集了,这样说是不是会更好理解一些?)
if (!this.active) {
return this.fn()
}
let parent: ReactiveEffect | undefined = activeEffect
// 缓存是否需要收集依赖
let lastShouldTrack = shouldTrack
...
try {
// 将上一层级的effect赋值给parent保存
this.parent = activeEffect
// 将当前层级的effect赋值给activeEffect
activeEffect = this
shouldTrack = true
// 是一个二进制类型的值,每一位用于标识当前 `effect` 嵌套层级的依赖收集的启用状态
trackOpBit = 1 << ++effectTrackDepth
// 如果当前嵌套层级不超过30
if (effectTrackDepth <= maxMarkerBits) {
initDepMarkers(this)
} else {
cleanupEffect(this)
}
return this.fn()
} finally {
if (effectTrackDepth <= maxMarkerBits) {
finalizeDepMarkers(this)
}
trackOpBit = 1 << --effectTrackDepth
// 通过parent返回上一层嵌套的effect
activeEffect = this.parent
// 回退之前的shouldTrack值
shouldTrack = lastShouldTrack
// 清空parent指针
this.parent = undefined
if (this.deferStop) {
this.stop()
}
}
}
stop() {
if (activeEffect === this) {
this.deferStop = true
} else if (this.active) {
cleanupEffect(this)
if (this.onStop) {
this.onStop()
}
this.active = false
}
}
}
在ReactiveEffect类中,注释里每一步已经写的比较详细了,这里我们可以重点关注里面的run方法,先顺一下整个run方法的流程:
- 首先,在run方法最开始执行的时候,我们判断当前副作用函数是否已经被卸载了,如果已经被卸载了,那我们只做执行函数的操作,不再进行下面的依赖收集逻辑。
- 然后,利用parent这个指针将当前的effect副作用函数上下文存储下来,确保了当effect中出现嵌套副作用函数时不会出现作用域问题。
- 用effectTrackDepth去记录当前嵌套的层级;trackOpBit是一个用二进制表示的数字,它在后面initDepMarkers以及finalizeDepMarkers函数中会用到,主要是用来维护该副作用函数在当前层级是否被追踪了。
- 根据嵌套层级来判断我们要执行的函数,如果嵌套层级大于30层,执行清除依赖的函数cleanupEffect,否则,执行initDepMarkers函数。
- 执行我们传入的函数fn,在执行fn时会对其中涉及到的响应式数据进行依赖追踪。
- 前面工作完成之后,执行finalizeDepMarkers函数,再将前面维护的一些变量一一复原。
过程中,我们涉及到了几个不明所以的函数,这里我们来一个一个看一看,逻辑也都不难。
cleanupEffect函数
我们先看当effect层级大于30层时会触发的cleanupEffect函数,相对来说比较简单: cleanupEffect函数主要是用来清空deps中所收集的副作用函数。
function cleanupEffect(effect: ReactiveEffect) {
const { deps } = effect
if (deps.length) {
for (let i = 0; i < deps.length; i++) {
deps[i].delete(effect)
}
deps.length = 0
}
}
initDepMarker函数
我们再看一下initDepMarker函数,该函数会在我们层级不大于30层时执行:
const initDepMarkers = ({ deps }: ReactiveEffect) => {
if (deps.length) {
// 遍历收集到的依赖,并为其上的w属性赋值,w代表wasTracked,值为通过或运算得到
for (let i = 0; i < deps.length; i++) {
deps[i].w |= trackOpBit
}
}
}
initDepMarkers函数就是遍历deps,为其每一项上的w属性,通过与trackOpBit进行或等运算进行赋值,主要是用来维护该副作用函数在当前层级是否被追踪了,w属性则会在下面的finalizeDepMarkers函数中被使用。
当我们后面需要判断某个dep是否被追踪过,那么我们就可以用当前层级对应的trackOpBit与dep的w属性进行&位运算:
const wasTracked = (dep: Dep): boolean => (dep.w & trackOpBit) > 0
这里可以举个例子:
比如我们在第一层嵌套的时候:
effectTrackDepth = 1
trackOpBit = 0000000000000000010
那此时当执行到initDepMarkers的时候,deps的每一项通过或等操作之后,对应的w属性即为:
dep.w = 0000000000000000010
当我们进入第二层嵌套的时候:
effectTrackDepth = 2
trackOpBit = 0000000000000000100
那么我们进行或等运算之后得到的dep.w就是0000000000000000100
那么如果我们想知道在第2层级,dep是否被追踪,就可以根据:
dep.w & trackOpBit = 0000000000000000100 & 0000000000000000100 = 100 (这里假设都是二进制表示)
那么dep.w & trackOpBit > 0 成立,说明该dep在第二层级被追踪过
finalizeDepMarkers函数
最后,当我们传入的函数执行完毕之后,也就是在run方法中调用this.fn之后,会进入到finally环节,执行finalizeDepMarkers函数,这个函数中,涉及了两个判断:wasTracked 和 newTracked,这里就用到了前面为dep赋值的w属性和n属性。
// 是否已被收集过
const wasTracked = (dep: Dep): boolean => (dep.w & trackOpBit) > 0
// 是否新收集
const newTracked = (dep: Dep): boolean => (dep.n & trackOpBit) > 0
const finalizeDepMarkers = (effect: ReactiveEffect) => {
const { deps } = effect
if (deps.length) {
let ptr = 0
for (let i = 0; i < deps.length; i++) {
const dep = deps[i]
// 如果某个dep之前被收集过,但是在新的一轮收集中没有收集到,说明这次的副作用函数在被执行时不可能触发对应的effect执行,可以直接将对应的副作用函数清除
if (wasTracked(dep) && !newTracked(dep)) {
dep.delete(effect)
} else {
deps[ptr++] = dep
}
dep.w &= ~trackOpBit
dep.n &= ~trackOpBit
}
deps.length = ptr
}
}
// 此处额外提一下n属性(newTracked的由来),主要就是在收集依赖阶段对新收集的依赖进行标记
function trackEffects(...) {
...
if (effectTrackDepth <= maxMarkerBits) {
if (!newTracked(dep)) {
dep.n |= trackOpBit
shouldTrack = !wasTracked(dep)
}
}
...
}
finalizeDepMarkers函数的工作主要就是清除无效的副作用函数。举个🌰:
const state = reactive({ a: 1, show: true });
effect(() => {
if (state.show) {
console.log(`a: ${state.a}`)
}
});
setTimeout(() => {
state.show = false
state.a++
}, 1000)
上面这个例子中,我们在控制台中只会打印一次1,这个根据逻辑我们也都能明白,可这里Vue3的内部还是有一点操作的:
- 首先,在第一次执行effect的时候,我们将
() =>{if(state.show){console.log(a: ${state.a})}}
这个函数传入了effect中,在effect中我们执行了this.fn,所以我们就在控制台中打印出了1。 - 而我们在执行this.fn的时候,实际上也就访问了对应state.show和state.a,从而触发了它们的getter,进行了依赖收集,此时这个effect的deps中保存的就是两个dep。
- 当我们定时器结束之后,首先改变了state.show,所以触发state.show的setter,在setter中会去查看state.show属性在get阶段收集到的副作用函数,并执行对应effect.fun方法。
- effect.fun方法则会触发上面提到的initDepMarkers方法,此时我们遍历deps上的两个dep,将他们都标记为已追踪wasTracked。
- 然后执行this.fn,但不同的是,因为我们的state.show已经被赋值为false了,所以在访问阶段不会访问到state.a,也就无法对state.a进行收集,这样一来,在执行finalizeDepMarkers函数的时候则会将对应的effect删除
- 最后,在执行state.a++时,虽然触发了setter,但是因为没有对应的effect,所以并不会在控制台进行打印。
总结
至此,我们总算是基本搞清楚了Vue3中的副作用函数effect到底是个什么东西,以及多层级嵌套副作用函数effect时,代码是如何运行的,同时也更详细的了解了副作用函数的收集和运行。
参考文章:
转载自:https://juejin.cn/post/7241842830888435770