【vue3源码】三、effectScope源码解析
前言
参考代码版本:vue 3.2.37
官方文档:https://vuejs.org/
关于为什么要有effectScope
可以参考RFC
使用示例
effectScope
可以对内部的响应式对象的副作用effect
进行统一管理。
const counter = ref(1)
const scope = effectScope()
scope.run(() => {
const doubled = computed(() => counter.value * 2)
watch(doubled, () => console.log(doubled.value))
watchEffect(() => console.log('Count: ', doubled.value))
})
// 处理掉当前作用域内的所有 effect
scope.stop()
effectScope
接收一个boolean
值,如果传true
代表游离模式,那么创建的scope
不会被父scope
收集,通俗来讲,如果是游离模式,那么scope
之间是不存在父子关系的,每一个scope
都是独立的。
export function effectScope(detached?: boolean) {
return new EffectScope(detached)
}
effectScope
返回一个EffectScope
实例。
EffectScope
export class EffectScope {
active = true
effects: ReactiveEffect[] = []
cleanups: (() => void)[] = []
parent: EffectScope | undefined
scopes: EffectScope[] | undefined
/**
* track a child scope's index in its parent's scopes array for optimized
* removal
*/
private index: number | undefined
constructor(detached = false) {
if (!detached && activeEffectScope) {
this.parent = activeEffectScope
this.index =
(activeEffectScope.scopes || (activeEffectScope.scopes = [])).push(
this
) - 1
}
}
run<T>(fn: () => T): T | undefined {
if (this.active) {
try {
activeEffectScope = this
return fn()
} finally {
activeEffectScope = this.parent
}
} else if (__DEV__) {
warn(`cannot run an inactive effect scope.`)
}
}
on() {
activeEffectScope = this
}
off() {
activeEffectScope = this.parent
}
stop(fromParent?: boolean) {
if (this.active) {
let i, l
for (i = 0, l = this.effects.length; i < l; i++) {
this.effects[i].stop()
}
for (i = 0, l = this.cleanups.length; i < l; i++) {
this.cleanups[i]()
}
if (this.scopes) {
for (i = 0, l = this.scopes.length; i < l; i++) {
this.scopes[i].stop(true)
}
}
// nested scope, dereference from parent to avoid memory leaks
if (this.parent && !fromParent) {
// optimized O(1) removal
const last = this.parent.scopes!.pop()
if (last && last !== this) {
this.parent.scopes![this.index!] = last
last.index = this.index!
}
}
this.active = false
}
}
}
constructor
EffectScope
构造器接收一个参数:detached
,默认值为false
,代表EffectScope
是否是游离状态。
constructor(detached = false) {
if (!detached && activeEffectScope) {
this.parent = activeEffectScope
this.index =
(activeEffectScope.scopes || (activeEffectScope.scopes = [])).push(
this
) - 1
}
}
如果detached
为false
,并且存在activeEffectScope
(activeEffectScope
是个全局变量)的情况,会将activeEffectScope
赋值给this.parent
,同时会将当前EffectScope
实例放入activeEffectScope.scopes
中,并将activeEffectScope.scopes
最后一个索引赋值给当前EffectScope
实例的index
属性。这样就可以通过this.index
来获取EffectScope
实例在父scope
中的索引位置。
run
run
方法可以接收一个函数参数。
run<T>(fn: () => T): T | undefined {
if (this.active) {
try {
activeEffectScope = this
return fn()
} finally {
activeEffectScope = this.parent
}
} else if (__DEV__) {
warn(`cannot run an inactive effect scope.`)
}
}
run
方法会首先对this.active
进行判断,如果this.active
为true
,也就是EffectScope
处于激活状态,那么会将this
赋给activeEffectScope
,然后执行fn
,并返回其执行结果。当fn
执行完毕后,将activeEffectScope
改为this.parent
。
on
on() {
activeEffectScope = this
}
on
方法会将activeEffectScope
指向当前EffectScope
实例。
off
off() {
activeEffectScope = this.parent
}
off
方法会将activeEffectScope
指向当前EffectScope
实例的父scope
。
stop
stop
函数的作用是清除scope
内的所有的响应式效果,包括子scope
。stop
接收一个boolean
类型的fromParent
参数,如果fromParent
为true
,stop
将不会删除在父scope
中的引用。
stop(fromParent?: boolean) {
if (this.active) {
let i, l
// 调用ReactiveEffect.prototype.stop,清除scope内所有响应式效果
for (i = 0, l = this.effects.length; i < l; i++) {
this.effects[i].stop()
}
// 触发scope销毁时的监听函数
for (i = 0, l = this.cleanups.length; i < l; i++) {
this.cleanups[i]()
}
// 销毁子scope
if (this.scopes) {
for (i = 0, l = this.scopes.length; i < l; i++) {
this.scopes[i].stop(true)
}
}
// 嵌套范围,从父级取消引用以避免内存泄漏
if (this.parent && !fromParent) {
// 获取父scope的中最后一个scope
const last = this.parent.scopes!.pop()
// last不是当前的scope
if (last && last !== this) {
// 将last放在当前scope在parent.scopes中的索引位置
this.parent.scopes![this.index!] = last
// last.index改为this.index
last.index = this.index!
}
}
// 修改scope的激活状态
this.active = false
}
}
stop
中的所有操作都要建立在scope
处于激活状态的基础上。首先遍历this.effects
执行元素的stop
方法。
for (i = 0, l = this.effects.length; i < l; i++) {
this.effects[i].stop()
}
scope.effects
存储的是在run
过程中获取到的ReactiveEffect
实例,这些ReactiveEffect
实例会通过一个recordEffectScope
方法被添加到scope.effects
中。
export function recordEffectScope(
effect: ReactiveEffect,
scope: EffectScope | undefined = activeEffectScope
) {
if (scope && scope.active) {
scope.effects.push(effect)
}
}
当遍历完scope.effects
或,会遍历scope.cleanups
属性。
for (i = 0, l = this.cleanups.length; i < l; i++) {
this.cleanups[i]()
}
scope.cleanups
中保存的是通过onScopeDispose
添加的scope
销毁监听函数。
export function onScopeDispose(fn: () => void) {
if (activeEffectScope) {
activeEffectScope.cleanups.push(fn)
} else if (__DEV__) {
warn(
`onScopeDispose() is called when there is no active effect scope` +
` to be associated with.`
)
}
}
如果当前scope
存在scopes
属性,意味着当前scope
存在子scope
,所以需要将所有子scope
也进行销毁。
if (this.scopes) {
for (i = 0, l = this.scopes.length; i < l; i++) {
this.scopes[i].stop(true)
}
}
如果当前scope
存在parent
的话,需要将scope
从其parent
中移除。
if (this.parent && !fromParent) {
// 获取父scope的中最后一个scope
const last = this.parent.scopes!.pop()
// last不是当前的scope
if (last && last !== this) {
// 将last放在当前scope在parent.scopes中的索引位置
this.parent.scopes![this.index!] = last
// last.index改为this.index
last.index = this.index!
}
}
这里的移除过逻辑是,先获取当前scope
的父scope
中的所有子scope
,然后取出最后一个scope
,这里用last
代表(注意last
不一定和当前scope
相同),如果last
和当前scope
不同的话,需要让last
替换当前scope
,这样我们就把当前scope
从其父scope
中移除了。这里仅仅替换是不够的,因为last.index
此时还是之前父scope
的最后一个索引,所以还需要把last.index
改为当前scope
在其父scope.scopes
中的位置。这样就完全移除了scope
。
最后,需要把scope
的激活状态改为false
。
this.active = false
getCurrentScope
getCurrentScope
可以获取当前处于活跃状态的EffectScope
。这里处于活跃状态的EffectScope
指得是当前执行环境在所处的那个EffectScope
。
export function getCurrentScope() {
return activeEffectScope
}