各位观众老爷,大家好!我是你们的老朋友,今天咱们来聊聊 Vue 3 源码里一个挺有意思的东西:effectScope
。 听名字就感觉它管的事情挺多的,那它到底是个什么东西,又怎么实现的呢? 别着急,咱们慢慢来,保证让大家听明白、学到手!
开场白:Vue 3 的“小管家” —— effectScope
在 Vue 3 的响应式系统中,effect
函数扮演着核心角色,它负责监听响应式数据的变化,并在数据发生改变时执行预定义的回调函数。 但是,当我们的应用变得复杂时,可能会创建大量的 effect
实例,这些实例彼此之间可能存在依赖关系,或者需要在特定时机一起停止。 如果我们手动管理这些 effect
实例,那简直就是一场噩梦!
这时候,effectScope
就闪亮登场了。 它可以看作是一个 "effect" 的容器,或者说是一个 "小管家",负责管理一组相关的 effect
实例,并提供统一的停止和清理机制。 有了 effectScope
,我们就可以更加方便地组织和控制响应式系统中的副作用。
effectScope
的基本概念和用法
首先,我们来看看 effectScope
的基本用法:
import { effectScope, ref, computed, onMounted } from 'vue';
export default {
setup() {
const count = ref(0);
const doubled = computed(() => count.value * 2);
const scope = effectScope();
scope.run(() => {
// 在 scope 内部创建的 effect 实例会自动添加到 scope 中
const logEffect = () => {
console.log('Count:', count.value, 'Doubled:', doubled.value);
};
effect(logEffect); // 创建一个 effect
// 也可以手动添加到 scope
scope.onScopeDispose(() => {
console.log('Scope is being disposed!');
});
});
onMounted(() => {
// 停止 scope 中的所有 effect
// scope.stop();
});
return {
count,
doubled,
scope
};
}
};
在这个例子中,我们创建了一个 effectScope
实例 scope
,并使用 scope.run()
方法执行一个函数。 在 scope.run()
内部创建的 effect
实例会自动添加到 scope
中。
effectScope
还提供了一些其他的方法:
stop()
: 停止scope
中的所有effect
实例。onScopeDispose(fn)
: 注册一个回调函数,在scope
被停止时执行。
源码剖析:effectScope
的实现原理
接下来,我们就深入 Vue 3 的源码,看看 effectScope
到底是怎么实现的。
// packages/reactivity/src/effectScope.ts
export let activeEffectScope: EffectScope | undefined
export class EffectScope {
active = true
effects: ReactiveEffect[] = []
cleanups: (() => void)[] = []
parent: EffectScope | undefined
/**
* record nested scopes so we can avoid recursively stopping the same scope
*/
scopes: EffectScope[] | undefined
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 {
this.on()
return fn()
} finally {
this.off()
}
} 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, avoid repeatedly patch
if (this.parent && !fromParent) {
// optimized: child scopes are always added to the end of the array so we
// can avoid O(n) scan
remove(this.parent.scopes!, this)
}
this.active = false
}
}
onScopeDispose(fn: () => void) {
if (this.active) {
this.cleanups.push(fn)
} else if (__DEV__) {
warn(`Cannot run onScopeDispose on inactive effect scope.`)
}
}
}
export function recordEffectScope(
effect: ReactiveEffect,
scope: EffectScope | undefined = activeEffectScope
) {
if (scope && scope.active) {
scope.effects.push(effect)
}
}
export function effectScope(detached?: boolean) {
return new EffectScope(detached)
}
export function getCurrentScope() {
return activeEffectScope
}
export function onScopeDispose(fn: () => void) {
if (activeEffectScope) {
activeEffectScope.onScopeDispose(fn)
} else if (__DEV__) {
warn(
`onScopeDispose() is called when there is no active effect scope` +
` to be associated with.`
)
}
}
我们来逐行分析一下这段代码:
-
activeEffectScope
: 这是一个全局变量,用于存储当前激活的effectScope
实例。 也就是说,在effectScope.run()
方法执行期间,activeEffectScope
会被设置为当前的effectScope
实例,而在effectScope.run()
方法执行完毕后,activeEffectScope
会被重置为之前的effectScope
实例。 -
EffectScope
类: 这是effectScope
的核心实现。它包含以下属性:active
: 一个布尔值,表示effectScope
是否处于激活状态。effects
: 一个数组,用于存储effectScope
管理的effect
实例。cleanups
: 一个数组,用于存储在effectScope
被停止时需要执行的回调函数。parent
: 指向父级EffectScope
的引用,用于处理嵌套的EffectScope
。scopes
: 一个数组,用于存储嵌套的EffectScope
。index
: 当前EffectScope
在父级scopes
数组中的索引。
-
EffectScope
构造函数: 在创建EffectScope
实例时,如果当前存在激活的effectScope
实例(即activeEffectScope
不为undefined
),则会将当前effectScope
实例添加到父级effectScope
实例的scopes
数组中,从而建立父子关系。detached
参数为true
的时候,不会和父级建立关系。 -
run(fn)
方法: 这个方法用于执行一个函数,并在执行期间将activeEffectScope
设置为当前的effectScope
实例。 这样,在fn
内部创建的effect
实例就可以自动添加到当前的effectScope
中。 -
stop(fromParent?: boolean)
方法: 这个方法用于停止effectScope
中的所有effect
实例。 它会遍历effects
数组,依次调用每个effect
实例的stop()
方法;然后遍历cleanups
数组,依次执行每个回调函数;最后,如果存在嵌套的effectScope
实例,则递归调用它们的stop()
方法。fromParent
参数用于解决嵌套effectScope
停止时的重复停止问题。 -
onScopeDispose(fn)
方法: 这个方法用于注册一个回调函数,在effectScope
被停止时执行。 它会将回调函数添加到cleanups
数组中。 -
recordEffectScope(effect, scope = activeEffectScope)
函数: 这个函数用于将一个effect
实例添加到指定的effectScope
中。 默认情况下,它会将effect
实例添加到当前激活的effectScope
中(即activeEffectScope
)。 -
effectScope(detached?: boolean)
函数: 这个函数用于创建一个effectScope
实例。 -
getCurrentScope()
函数: 这个函数用于获取当前激活的effectScope
实例。 -
onScopeDispose(fn)
函数: 这个函数用于注册一个回调函数,在当前激活的effectScope
实例被停止时执行。
Map
的角色? 关联 effect
实例的方式
看到这里,有的小伙伴可能会问了: "你说 effectScope
利用 Map
关联 effect
实例,但是我在源码里没看到 Map
啊?"
别着急,其实 effectScope
并没有直接使用 Map
来存储 effect
实例。 它是使用数组 effects: ReactiveEffect[]
来存储 effect
实例的。
但是,effect
实例本身内部会维护一个对所属 effectScope
的引用。 具体来说,在 effect
实例创建时,recordEffectScope
函数会将 effect
实例添加到当前激活的 effectScope
的 effects
数组中。 同时,effect
实例也会记录下自己所属的 effectScope
。
这种方式可以看作是一种隐式的关联,它避免了使用 Map
带来的额外开销,同时也能够满足 effectScope
对 effect
实例的管理需求。
stop
时的高效清理:如何避免内存泄漏
effectScope
的一个重要作用就是在 stop
时进行高效清理,避免内存泄漏。 它是通过以下几个方面来实现的:
-
停止
effect
实例: 在effectScope.stop()
方法中,会遍历effects
数组,依次调用每个effect
实例的stop()
方法。 这样可以确保所有的effect
实例都被正确停止,从而解除对响应式数据的依赖,避免内存泄漏。 -
执行清理回调函数: 在
effectScope.stop()
方法中,还会遍历cleanups
数组,依次执行每个回调函数。 这些回调函数可以用于执行一些额外的清理操作,例如取消订阅事件、释放资源等。 -
递归停止嵌套的
effectScope
: 如果存在嵌套的effectScope
实例,effectScope.stop()
方法会递归调用它们的stop()
方法。 这样可以确保所有的effectScope
实例都被正确停止,从而避免内存泄漏。 -
移除父级
scopes
的引用: 为了避免重复stop,stop
的时候会将自己从父级的scopes
中移除。
通过这些机制,effectScope
可以确保在 stop
时进行高效清理,避免内存泄漏,从而提高应用的性能和稳定性。
使用表格总结EffectScope
类
属性 | 类型 | 描述 |
---|---|---|
active |
boolean |
指示此作用域是否处于活动状态。如果为 false ,则作用域不再收集 effect 或提供依赖清理。 |
effects |
ReactiveEffect[] |
此作用域收集的 effect 的数组。 |
cleanups |
(() => void)[] |
作用域停止时要调用的函数数组。用于清理资源或取消副作用。 |
parent |
EffectScope | undefined |
父 effect 作用域,如果此作用域是嵌套在另一个作用域中创建的。 |
scopes |
EffectScope[] | undefined |
此作用域创建的子作用域数组。用于管理嵌套作用域的生命周期。 |
index |
number | undefined |
当前EffectScope 在父级scopes 数组中的索引。 |
方法 | 参数 | 返回值 | 描述 |
---|---|---|---|
constructor |
detached?: boolean |
创建一个新的 effect 作用域。如果 detached 为 true ,则作用域不会自动链接到活动作用域。 |
|
run |
fn: () => T |
T | undefined |
在作用域的上下文中执行一个函数。这允许作用域捕获在函数中创建的 effect。 |
on |
将此作用域设置为活动作用域。 | ||
off |
将活动作用域重置为其父作用域。 | ||
stop |
fromParent?: boolean |
停止作用域中的所有 effect 和清理函数。这会禁用作用域,并防止它收集更多 effect。 fromParent 参数用于解决嵌套effectScope 停止时的重复停止问题。 |
|
onScopeDispose |
fn: () => void |
注册一个清理函数,该函数将在作用域停止时调用。 |
总结
effectScope
是 Vue 3 响应式系统中的一个重要组成部分,它提供了一种方便的方式来管理一组相关的 effect
实例,并提供统一的停止和清理机制。 通过深入理解 effectScope
的实现原理,我们可以更好地理解 Vue 3 的响应式系统,并能够更加灵活地使用它来构建高性能、高可靠性的应用。
好了,今天的讲座就到这里。 希望大家有所收获,也欢迎大家多多交流学习! 咱们下次再见!