认识
一、认识
ReactiveEffect
是 Vue 3
响应式系统的核心类之一,它封装了一个响应式计算(effect
), 使得该计算在依赖的响应式数据发生变化时能够重新执行, 从而实现组件更新、计算属性、watcher
等功能。具体工作流为: 1. 封装响应式副作用函数, 通过 ReactiveEffect
对象封装 effectFn
副作用函数, 使其能在依赖变化时重新执行; 2. 依赖收集, 在执行 effectFn
副作用函数过程中, 将访问的响应式数据(通过 getter
拦截)收集到依赖集合中, 同时, 每个响应式数据内部通过一个依赖集合(Dep
)记录当前活跃的 effect
; 3. 调度更新, 当响应式数据发生变化时, 会遍历 Dep
依赖集合, 触发 effect
重新执行, 或通过调度器进行批量更新; 4. 清理依赖, 提供 stop()
方法,能在 effect
停止后清理所有依赖,避免内存泄漏和不必要的更新。
一、ReactiveEffect
底层结构
let activeEffect = null;
const targetMap = new WeakMap();
export class ReactiveEffect{
constructor(fn, scheduler){
this.fn = fn;
this.scheduler = scheduler;
this.deps = [];
this.active = true;
this.onStop = null;
}
run(){
if (!this.active) {
return this.fn();
}
try {
activeEffect = this;
return this.fn();
} finally {
activeEffect = null;
}
}
stop(){
if (this.active) {
this.deps.forEach(dep => {
dep.delete(this);
});
this.deps.length = 0;
this.active = false;
if (this.onStop) {
this.onStop();
}
}
}
}
function track(target, key) {
if (!activeEffect) return;
let depsMap = targetMap.get(target);
if (!depsMap) {
depsMap = new Map();
targetMap.set(target, depsMap);
}
let dep = depsMap.get(key);
if (!dep) {
dep = new Set();
depsMap.set(key, dep);
}
if (!dep.has(activeEffect)) {
dep.add(activeEffect);
activeEffect.deps.push(dep);
}
}
function trigger(target, key, newVal) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
const dep = depsMap.get(key);
if (dep) {
dep.forEach(effect => {
if (effect.scheduler) {
effect.scheduler(effect);
} else {
effect.run();
}
});
}
}
二、属性方法
-
activeEffect
: 当前激活的全局副作用(effect
)函数。 -
targetMap
:targetMap
是Vue 3
响应式系统中的一个关键数据结构, 用于存储所有响应式对象(即通过reactive
包装的目标对象)的依赖关系。它将每个响应式对象(target
)映射到一个内部Map
,该Map
以属性名为key
,将对应属性的依赖集合(Set
)作为value
。内部结构如下:targetMap = WeakMap {
target1 => Map {
key1 => Set([effect1, effect2]),
key2 => Set([effect3]),
...
},
target2 => Map {
...
},
...
}使用
WeakMap + Map + Set
这种嵌套数据结构,可以非常高效地记录和查找每个响应式对象及其各个属性对应的effect
。WeakMap
的使用保证了如果目标对象不再被引用,相关的依赖也可以被垃圾回收,不会造成内存泄漏。 -
fn
: 保存传入的effect
函数(即用户的副作用函数) -
lazy
: 如果为true
,则effect
不会立即执行(例如用于computed
计算)。 -
deps
: 一个数组,记录当前effect
收集到的所有依赖(dep
对象)。每个dep
通常是一个Set
,包含了多个effect
。这样,在effect
停止(stop
)时可以遍历deps
,清理自身在各个dep
中的注册。 -
schedular
: 可选的调度器函数, 当依赖变化时会调用该函数而不是直接重新运行effect
。这使得Vue
能够对更新进行批量调度或延迟执行。 -
run()
:ReactiveEffect
的核心方法,当调用run()
时,会将当前effect
设为全局活跃的effect
,并执行effectFn
。在执行期间,会触发所有响应式数据的getter
,从而进行依赖收集(通过track()
)。执行完后,会将effect
从全局effect
堆栈中移除,返回effectFn
的返回值。如果effect
处于非活跃状态(active
为false
),则直接执行effectFn
,不会再收集依赖。 -
stop()
: 停止该effect
,清理所有依赖,并将active
设为false
。遍历effect.deps
,清除effect
在各个dep
中的注册,防止内存泄漏和不必要的更新通知。如果有onStop
回调,也会在此时调用。
三、依赖收集 track
: 当 ReactiveEffect
实例调用 run()
时, 会设置全局变量(通常为 activeEffect
)为当前 effect
, 执行 effectFn
副作用函数。在 effectFn
执行过程中, 会触发所有响应式数据的 getter
,会通过 track
进行依赖收集。track()
会把当前全局 activeEffect
添加到对应的依赖集合(dep
)中,同时 effect
也将该 dep
存入自己的 deps
数组中,这样形成双向关联。双向关联, 在依赖收集时,每个 effect
还会记录下自己被添加到的依赖集合(Set
)中,这使得在 effect
停止(stop
)时能够方便地遍历和清理所有依赖。effectFn
执行完后, 会将 effect
从全局 effect
堆栈中移除, 返回 effectFn
的返回值。
四、触发更新 trigger
: 当响应式数据被修改时(通过 set
拦截器),会调用 trigger()
函数。trigger()
遍历该响应式数据对应的 dep
集合,依次取出 effect
,然后: 如果 effect
配置了 scheduler
,则调用 scheduler(effect)
; 否则直接调用 effect.run()
,重新执行 effectFn
,更新依赖视图或计算属性。
五、清理依赖: 当 effect.stop()
被调用时,会遍历 effect.deps
数组,逐个从每个 dep
中移除该 effect
,这样即使响应式数据发生变化,也不会触发已经停止的 effect
。清理完成后,将 effect.deps
清空,active
置为 false
。
六、调度器与批量更新: ReactiveEffect
可以配置调度器函数(scheduler
)。调度器使得当多个响应式数据变化时,能够将更新任务合并、延迟或进行自定义调度,从而避免重复调用 effect.run()
导致性能问题。例如,在组件渲染中,调度器可以将更新任务放入微任务队列中,以实现批量更新和时间片控制。
二、变量细节
2.1 activeEffect
activeEffect
记录当前激活的副作用函数, 基本逻辑如下:
let activeEffect;
function effect(fn,options = {}){
const effectFn = ()=>{
activeEffect = effectFn;
}
};
2.2 effectStack
2.3 targetMap
targetMap
存储响应式数据对应的副作用函数。结构如下:
targetMap <WeakMap>{
target1: <Map>{
key1 <Set> [effectFn1,effectFn2,……],
key2 <Set> [effectFn1,effectFn2,……]
},
target2: {
key1 <Set> [effectFn1,effectFn2,……],
key2 <Set> [effectFn1,effectFn2,……]
}
}
2.4 shouldTrack
2.5 ITERATE_KEY
2.6 MAP_KEY_ITERATE_KEY
三、函数细节
3.1 effectFn
effectFn
是 effect
真正的副作用函数, 基本机制如下:
function effect(fn,options){
const effectFn = ()=>{
activeEffect = effectFn; // 执行 effectFn , 立马将 activeEffect 赋值为当前的 effectFn
const res = fn();
return res;
}
effectFn.deps = []; // 用于存储所有包含当前副作用函数的依赖集合
effectFn.options = options;
return effectFn;
}
3.2 cleanup
在每次副作用函数执行时, 根据 effectFn.deps
获取所有相关联的依赖集合, 进而将副作用函数从依赖集合中移除。这样可以避免产生遗留的副作用函数。
function cleanup(effectFn){
for(let i=0; i<effectFn.deps.length; i++){
const deps = effectFn.deps[i];
deps.delete(effectFn);
}
effectFn.deps.length = 0;
}
每次副作用函数执行时, 需要清除副作用哈数相关联的依赖
function effect(fn,options = {}){
const effectFn = ()=>{
activeEffect = effectFn;
cleanup(effectFn);
const res = fn();
return res;
}
}
3.3 track
track
用于注册与 key
相关的副作用函数
3.4 trigger
trigger
用于取出与 key
相关的副作用函数并执行。基本逻辑如下:
function trigger(target,key){
const depsMap = targetMap.get(target);
if(depsMap){
return
}
const effects = depsMap.get(key);
const effectsToRun = new Set(); // 通过 Set 集合存储 effects 并去重, 遍历 Set 而不是直接遍历 effects ,避免 `forEach` 时的无限递归。
// trigger 触发执行的副作用函数与当前正在执行的副作用函数相同, 则不触发执行,避免无限递归调用和栈溢出。
effects && effects.forEach((effectFn)=>{
if(effectFn !== activeEffect){
effectsToRun.add(effectFn);
}
});
effectsToRun.forEach(effectFn => effectFn());
}