各位观众老爷,欢迎来到今天的 Vue 3 源码解剖现场!今天我们要聊的主题是 Vue 3 响应式系统的核心组件之一:ReactiveEffect
。 别担心,虽然听起来很吓人,但其实它就像一个勤劳的“数据管家”,负责维护着数据变化和视图更新之间的和谐关系。
准备好了吗?让我们一起深入源码,看看这位“管家”是如何工作的。
ReactiveEffect:响应式系统的灵魂舞者
ReactiveEffect
类是 Vue 3 响应式系统的核心,它负责将依赖(dep
,Dependency)和副作用(effect
,执行函数)连接起来,形成一个高效的“数据-视图”同步机制。 简单来说,它做了两件事:
- 追踪依赖: 当一个
effect
函数执行时,它会记录下其中访问了哪些响应式数据。 - 触发更新: 当这些响应式数据发生变化时,它会重新执行这个
effect
函数,从而更新视图。
1. ReactiveEffect 类的基本结构
我们先来看看 ReactiveEffect
类的基本骨架:
class ReactiveEffect<T = any> {
active = true; // effect是否激活
deps: Dep[] = []; // 存储依赖的 Set 集合
parent: ReactiveEffect | undefined = undefined;
options: ReactiveEffectOptions; // 一些选项,如调度器、是否lazy等
constructor(
public fn: () => T, // 实际执行的函数,也就是副作用函数
scheduler?: EffectScheduler | null,
scope?: EffectScope | undefined
) {
this.options = { scheduler };
this.scheduler = scheduler;
this.scope = scope;
}
run() {
if (!this.active) { // 如果 effect 已经停止,直接执行 fn,不追踪依赖
return this.fn();
}
let parent: ReactiveEffect | undefined = activeEffect;
let lastShouldTrack = shouldTrack;
try {
this.parent = parent;
activeEffect = this; // 将当前 effect 设为激活状态
shouldTrack = true; // 允许追踪依赖
cleanupEffect(this); // 清理之前的依赖
return this.fn(); // 执行副作用函数,期间会触发 get 操作,收集依赖
} finally {
activeEffect = parent; // 恢复之前的激活状态
shouldTrack = lastShouldTrack; // 恢复之前的追踪状态
}
}
stop() {
if (this.active) {
cleanupEffect(this); // 清理所有依赖
if (this.options.onStop) {
this.options.onStop(); // 执行 stop 回调
}
this.active = false; // 标记为停止状态
}
}
}
这里几个关键的属性和方法需要重点关注:
active
: 标志effect
是否处于激活状态,只有激活状态的effect
才会追踪依赖和执行更新。deps
: 存储当前effect
依赖的所有Dep
实例,Dep
实例负责管理依赖于某个响应式数据的所有effect
。fn
: 实际执行的副作用函数,也就是我们希望在数据变化时重新执行的函数。run()
: 执行effect
函数的核心方法,负责设置全局状态、清理旧依赖、执行副作用函数以及恢复全局状态。stop()
: 停止effect
函数的执行,并清理所有相关的依赖。cleanupEffect()
: 用于清理副作用effect的所有依赖关系,防止内存泄漏
2. 依赖追踪:track
函数
当我们在 effect
函数中访问响应式数据时,会触发该数据的 get
操作。 在 get
操作中,Vue 会调用 track
函数来建立依赖关系。 track
函数的核心逻辑如下:
let activeEffect: ReactiveEffect | undefined;
let shouldTrack = true;
export function track(target: object, type: TrackOpTypes, key: unknown) {
if (!isTracking()) {
return;
}
let depsMap = targetMap.get(target);
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()));
}
let dep = depsMap.get(key);
if (!dep) {
depsMap.set(key, (dep = createDep()));
}
trackEffects(dep);
}
export function isTracking() {
return shouldTrack && activeEffect !== undefined;
}
export function trackEffects(dep: Dep) {
if (!dep.has(activeEffect!)) {
dep.add(activeEffect!);
activeEffect!.deps.push(dep);
}
}
其中,targetMap
是一个全局的 WeakMap
,用于存储所有响应式数据的依赖关系。 其结构大致如下:
targetMap: {
target(object): { // 响应式对象
depsMap(Map): {
key(string | symbol): Dep // 响应式属性
}
}
}
track
函数的执行流程如下:
- 检查是否需要追踪依赖 (
isTracking()
函数)。 只有当shouldTrack
为true
且activeEffect
存在时,才会进行依赖追踪。 - 从
targetMap
中获取当前响应式对象对应的depsMap
。 如果不存在,则创建一个新的depsMap
。 - 从
depsMap
中获取当前响应式属性对应的Dep
实例。 如果不存在,则创建一个新的Dep
实例。 - 调用
trackEffects
函数,将当前激活的effect
添加到Dep
实例中,并将Dep
实例添加到effect
的deps
数组中。
这样就完成了 dep
和 effect
之间的双向绑定。
3. 触发更新:trigger
函数
当响应式数据发生变化时,会触发该数据的 set
操作。 在 set
操作中,Vue 会调用 trigger
函数来触发更新。 trigger
函数的核心逻辑如下:
export function trigger(target: object, type: TriggerOpTypes, key: unknown, newValue?: any, oldValue?: any) {
const depsMap = targetMap.get(target);
if (!depsMap) {
return; // 没有依赖,直接返回
}
let deps: (Dep | undefined)[] = [];
deps.push(depsMap.get(key)); // 拿到key对应的dep
const effects: ReactiveEffect[] = [];
for (const dep of deps) {
if (dep) {
effects.push(...dep);
}
}
runEffects(effects);
}
export function runEffects(effects: ReactiveEffect[]) {
const seen = new Set();
for (const effect of effects) {
if (effect.scheduler) {
effect.scheduler(); // 如果有调度器,则调用调度器
} else {
if (!seen.has(effect)) {
effect.run(); // 否则直接执行 effect
seen.add(effect);
}
}
}
}
trigger
函数的执行流程如下:
- 从
targetMap
中获取当前响应式对象对应的depsMap
。 如果不存在,则表示该对象没有依赖,直接返回。 - 从
depsMap
中获取当前响应式属性对应的Dep
实例。 - 遍历
Dep
实例中的所有effect
,依次执行它们。 - 如果
effect
存在调度器 (scheduler
),则调用调度器。 否则,直接调用effect.run()
方法。
4. cleanupEffect
函数:依赖清理专家
cleanupEffect
函数是 ReactiveEffect
类中非常重要的一个方法,它负责清理 effect
之前收集的依赖。 它的主要作用是:
- 防止内存泄漏: 如果不清理旧的依赖,
effect
会一直持有对旧Dep
实例的引用,导致这些Dep
实例无法被垃圾回收。 - 避免不必要的更新: 如果不清理旧的依赖,当旧的依赖数据发生变化时,
effect
可能会被错误地触发,导致不必要的更新。
cleanupEffect
函数的核心逻辑如下:
function cleanupEffect(effect: ReactiveEffect) {
const { deps } = effect;
if (deps.length) {
for (let i = 0; i < deps.length; i++) {
deps[i].delete(effect); // 从 dep 中移除 effect
}
deps.length = 0; // 清空 effect 的 deps 数组
}
}
cleanupEffect
函数的执行流程如下:
- 遍历
effect
的deps
数组,依次从每个Dep
实例中删除当前effect
。 - 清空
effect
的deps
数组。
cleanupEffect
函数在以下场景会被调用:
- 在
effect.run()
方法执行之前。 - 在
effect.stop()
方法执行时。
5. stop
函数:优雅的停止
stop
函数用于停止 effect
的执行,并清理所有相关的依赖。 它的核心逻辑如下:
stop() {
if (this.active) {
cleanupEffect(this); // 清理所有依赖
if (this.options.onStop) {
this.options.onStop(); // 执行 stop 回调
}
this.active = false; // 标记为停止状态
}
}
stop
函数的执行流程如下:
- 检查
effect
是否处于激活状态。 只有激活状态的effect
才能被停止。 - 调用
cleanupEffect
函数,清理所有相关的依赖。 - 如果
effect
存在onStop
回调函数,则执行该回调函数。 - 将
effect
的active
属性设置为false
,表示该effect
已经停止。
案例分析:一个简单的计数器
为了更好地理解 ReactiveEffect
的工作原理,我们来看一个简单的计数器案例:
<template>
<div>
<p>Count: {{ count }}</p>
<button @click="increment">Increment</button>
</div>
</template>
<script>
import { ref, onMounted, onUnmounted } from 'vue';
export default {
setup() {
const count = ref(0);
const increment = () => {
count.value++;
};
onMounted(() => {
console.log('Component mounted');
});
onUnmounted(() => {
console.log('Component unmounted');
});
return {
count,
increment,
};
},
};
</script>
在这个案例中,count
是一个响应式数据。 当我们点击 "Increment" 按钮时,count.value
会被更新,从而触发视图的更新。
让我们来分析一下在这个案例中 ReactiveEffect
是如何工作的:
- 当组件渲染时,Vue 会创建一个
ReactiveEffect
实例,并将组件的渲染函数作为fn
属性传入。 - 在渲染函数执行过程中,会访问
count.value
,从而触发count
的get
操作。 - 在
get
操作中,track
函数会被调用,将当前的ReactiveEffect
实例添加到count
对应的Dep
实例中。 - 当我们点击 "Increment" 按钮时,
count.value
会被更新,从而触发count
的set
操作。 - 在
set
操作中,trigger
函数会被调用,触发count
对应的Dep
实例中的所有effect
。 ReactiveEffect
实例的run
方法会被执行,重新执行组件的渲染函数,从而更新视图。
当组件卸载时,Vue 会调用 ReactiveEffect
实例的 stop
方法,清理所有相关的依赖,防止内存泄漏。
ReactiveEffect、Dep、targetMap的关系
我们可以用一张表格来总结 ReactiveEffect
、Dep
和 targetMap
之间的关系:
组件 | 作用 | 关键属性/方法 |
---|---|---|
ReactiveEffect |
将依赖和副作用函数连接起来,负责追踪依赖和触发更新。 | active : 标志 effect 是否处于激活状态;deps : 存储依赖的 Dep 实例;fn : 副作用函数;run() : 执行副作用函数;stop() : 停止 effect 的执行;cleanupEffect() : 清理依赖。 |
Dep |
存储依赖于某个响应式数据的所有 effect 。 |
add(effect: ReactiveEffect) : 添加 effect;delete(effect: ReactiveEffect) : 删除 effect;has(effect: ReactiveEffect) : 是否包含 effect。 |
targetMap |
存储所有响应式数据的依赖关系。 | get(target: object) : 获取 target 对应的 depsMap;set(target: object, depsMap: Map) : 设置 target 对应的 depsMap。 |
总结
ReactiveEffect
类是 Vue 3 响应式系统的核心组件,它负责将依赖和副作用函数连接起来,形成一个高效的“数据-视图”同步机制。 通过 track
函数建立依赖关系,通过 trigger
函数触发更新,通过 cleanupEffect
函数清理依赖,通过 stop
函数停止 effect
的执行。 理解 ReactiveEffect
的工作原理,可以帮助我们更好地理解 Vue 3 响应式系统的底层机制,从而更好地使用 Vue 3 进行开发。
好了,今天的 Vue 3 源码解剖就到这里。 希望大家通过今天的学习,能够对 ReactiveEffect
有更深入的理解。 感谢大家的观看,我们下期再见!