阅读准备
本文使用的
vue
版本为3.2.26
。在阅读effectScope
源码之前,我们需要知道它的特性,可以通过阅读单例测试源码或者是阅读官网的API
了解特性,推荐阅读单例,了解特性在后面阅读时才能更好理解。
在vue3
中可以使用effectScope
函数创建一个统一管理effect
的对象。注入到这个对象的effect
会被记录到effects
属性中,当执行stop
方法时,被记录的所有effect
都会停止监听。既然是管理effect
自然也就包括computed
和deferredComputed
,因为他们内部是通过effect
实现的,具体参考之前文章vue3-computed源码解析。
effectScope
也会记录子effectScope
,当停止监听时子级也会同时停止。这个api
很少人使用,我们稍微简单看看使用方式:
import { effectScope, reactive } from 'vue'
let dummy, doubled
const counter = reactive({ num: 0 })
const scope = effectScope()
scope.run(() => {
effect(() => (dummy = counter.num))
// 子级scope
effectScope().run(() => {
effect(() => (doubled = counter.num * 2))
})
})
// 收集到的effect
console.log(scope.effects.length) // 1
// 收集到的子级scope
console.log(scope.scopes!.length) // 1
console.log(dummy) // 0
console.log(doubled) // 0
counter.num = 7
console.log(dummy) // 7
console.log(doubled) // 14
// 停止所有监听,子级都会停止
scope.stop()
counter.num = 6
console.log(dummy) // 7
console.log(doubled) // 14
effectScope
有一个可选参数为boolean
,当传入true
时表示阻断与父级的联系,阻断后这个scope
对象将不会与父级关联,成为独立的scope
。父级的stop
也不会影响到它。比如:
import { effectScope, reactive } from 'vue'
let dummy, doubled
const counter = reactive({ num: 0 })
const scope = effectScope()
scope.run(() => {
effect(() => (dummy = counter.num))
// 阻断父级收集
effectScope(true).run(() => {
effect(() => (doubled = counter.num * 2))
})
})
console.log(scope.effects.length) // 1
console.log(scope.scopes) // undefined
console.log(dummy) // 0
console.log(doubled) // 0
counter.num = 7
console.log(dummy) // 7
console.log(doubled) // 14
// 停止所有监听,子级被阻断
scope.stop()
counter.num = 6
console.log(dummy) // 7
console.log(doubled) // 12
effectScope
现在我们了解effectScope
的作用与特性了,接下来我们看看它是怎么实现的
// 当前正在执行的scope
let activeEffectScope: EffectScope | undefined
// 可能轮询调用,记录栈
const effectScopeStack: EffectScope[] = []
export class EffectScope {
// 是否被停止了
active = true
// 记录的effects
effects: ReactiveEffect[] = []
// 用户注入的清除函数
cleanups: (() => void)[] = []
// 父级
parent: EffectScope | undefined
// 子级scopes
scopes: EffectScope[] | undefined
// 索引,方便删除,记住当前scope在父级中的位置
private index: number | undefined
// 是否需要阻断
constructor(detached = false) {
// 如果不阻断,则要记录父级关系
if (!detached && activeEffectScope) {
// 当前正在使用的effect作用域作为父级
this.parent = activeEffectScope
// 记住当前作用域在父级中的位置
this.index =
(activeEffectScope.scopes || (activeEffectScope.scopes = [])).push(
this
) - 1
}
}
// 用户执行操作
run<T>(fn: () => T): T | undefined {
if (this.active) {
try {
// 打开effect收集
this.on()
// 执行用户方法
return fn()
} finally {
// 关闭收集
this.off()
}
} else if (__DEV__) {
warn(`cannot run an inactive effect scope.`)
}
}
// 开启收集
on() {
if (this.active) {
// 将当前scope入栈,并设置为当前effect
effectScopeStack.push(this)
activeEffectScope = this
}
}
// 关闭收集
off() {
if (this.active) {
// 出栈,并恢复正在使用的effectScope
effectScopeStack.pop()
activeEffectScope = effectScopeStack[effectScopeStack.length - 1]
}
}
// 统一停止收集到的effect监听,传入是否是父级删除标志
stop(fromParent?: boolean) {
if (this.active) {
// 停止所有effect
this.effects.forEach(e => e.stop())
// 执行用户注册的清除函数
this.cleanups.forEach(cleanup => cleanup())
// 停止所有子级scope,并传入是父级停止
if (this.scopes) {
this.scopes.forEach(e => e.stop(true))
}
// 停止监听,如果是自身停止,也要从父级中删除,防止内存泄漏
if (this.parent && !fromParent) {
// 直接从父级中删除一个,判断是否是当前,如果不是则跟当前替换位置
const last = this.parent.scopes!.pop()
if (last && last !== this) {
this.parent.scopes![this.index!] = last
last.index = this.index!
}
}
// 标识当前已经停止
this.active = false
}
}
}
// 创建effect作用域对象
export function effectScope(detached?: boolean) {
return new EffectScope(detached)
}
effectScope
函数只是创建EffectScope
对象并抛出。这个类里面的关联实现并不多,相对比较简单。
scope
记录关联的effect
与子级scope
是在run
方法实现上。用户传入操作函数即可记录函数中创建的子级scope
与effect
。他们是如何实现的呢,在执行前会通过on
函数开启收集,记录当前scope
,并让scope
入栈,然后再执行操作函数。执行完毕执行off
关闭收集,恢复上一次当前scope
。
scope
实例化时会判断当前是否需要阻断,如果不阻断则在记录当前scope
与实例化的scope
父子关系。并记录实例化的scope
的父级存储在parent
,以及自身在父级scopes
的位置,方便管理。这一步就收集到了子级scope
。
recordEffectScope
在之前vue3-effect源码解析一章有看到,实例化effect
时会执行recordEffectScope(this, scope)
,这个方法用来记录effect
与scope
的关系的,我们看看它是如何实现的
// 记录effect作用域
export function recordEffectScope(
effect: ReactiveEffect,
scope?: EffectScope | null
) {
// 如果没有指定则默认是当前激活的scope
scope = scope || activeEffectScope
if (scope && scope.active) {
// 记录
scope.effects.push(effect)
}
}
当effect
实例化时会判断是否有明确指定scope
,如果没有则使用当前scope
作为他的作用域。然后记录到effects
属性上,这一步就收集到了关联的effect
stop
scope.stop
会停止自身收集到的effect
监听,然后再调用子级stop
函数停止子级的effect
监听,这样相关联的所有effect
都被停止监听。
scope.stop
有一个参数标记当前是自身停止的还是父级调用停止收集的,如果是自身调用则解除自身与父级的关联,父级调用的则不需要解除与父级的关联。也就是说停止关联只会影响父级的scopes
,自身并不影响。并通过active
属性将scope
标识为已停止。
onScopeDispose
scope
通过onScopeDispose
方法来注册用户的当stop
时的回调,并收集在scope
的cleanups
属性上。注意onScopeDispose
只能在scope.run
的操作函数注册。下面我们看看源码:
// 注册stop 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
,如果存在则记录到cleanups
属性上,当scope.stop
是就执行它们。
getCurrentScope
vue
还提供getCurrentScope
方法来访问当前scope
// 获取当前作用域
export function getCurrentScope() {
return activeEffectScope
}
到这里effectScope
就讲完了,emm,vue3-reactivity源码解析系列也更新完了。后面有时间我再开个专题vue3-runtime-core源码解析。完结撒花。