Vue3 源码阅读(4)- effect
effect 是 @vue/reactivity 包中另外一个非常重要的实现,watchEffect,watch,computed 都是基于这个实现的,该 api 是公开的但是并没有写入文档
effect 就是副作用的意思,其作用很容易理解,我放一段代码:
1 | <script setup> |
我们打开控制台,上面会立刻打印:
1 | count: 0 hello: hello |
点击一下 add 按钮,控制台会再次打印两行:
1 | count: 1 hello: hello |
嗯?为什么会打印两次?watchEffect 传入该回调应该只会执行一次啊
因为 effect 是偏底层的接口,它只负责最基础的行为:追踪变化,调用回调,并不会把多次响应合并起来,后面再看 watchEffect 是如何实现该特性的
effect
我们先来看 effect 的实现,其内部其实挺简单的
它接收两个参数,回调 fn 和配置选项 options:
1 | export function effect<T = any>( |
如果
fn上有effect:1
2
3if ((fn as ReactiveEffectRunner).effect) {
fn = (fn as ReactiveEffectRunner).effect.fn;
}则取该
effect上的fn构建一个
ReactiveEffect:1
2
3
4
5const _effect = new ReactiveEffect(fn);
if (options) {
extend(_effect, options);
if (options.scope) recordEffectScope(_effect, options.scope);
}如果有
options,会使用options覆盖副作用_effect中的默认配置如果存在区域
scope,则将该副作用记录到区域中默认立即执行一次副作用:
1
2
3if (!options || !options.lazy) {
_effect.run();
}构建
runner并返回:1
2
3const runner = _effect.run.bind(_effect) as ReactiveEffectRunner;
runner.effect = _effect;
return runner;runner其实就是副作用的回调方法的代理(run),并且上面记录了副作用对象本身(runner.effect = _effect)
ReactiveEffect
上一小节构建的 ReactiveEffect,我们来看看其实现
构造器
构造器很简单,就是接收一个回调 fn、调度器 scheduler 和副作用区域 scope
调度器的作用比较偏底层,在 trigger 触发响应时会优先使用 scheduler,不存在才调用 run
构造器代码如下:
1 | class ReactiveEffect<T = any> { |
这里使用了在构造器参数里定义类成员变量的语法,fn 和 scheduler 都是可以通过 this.xxx 访问的
构造器内部仅仅只是记录了当前副作用的区域 scope,不传则没有效果
run
该函数是核心中的核心
首先判断自身是否活跃:
1
2
3if (!this.active) {
return this.fn();
}如果当前副作用不活跃,直接返回执行回调的结果
在当前执行上下文中记录父副作用:
全局变量
activeEffect记录了执行中的副作用假设副作用
A执行时触发了副作用B副作用
B刚刚开始执行时,activeEffect就是其父副作用A,记作parent1
let parent: ReactiveEffect | undefined = activeEffect;
记录上一轮的是否需要追踪 flag:
全局变量
shouldTrack记录了当前执行中的副作用是否应该追踪依赖(响应式成员)的变化1
let lastShouldTrack = shouldTrack;
在副作用链上寻找,是否已经存在自身
1
2
3
4
5
6while (parent) {
if (parent === this) {
return;
}
parent = parent.parent;
}如果存在则直接跳过副作用的执行(这里是为了防止出现死循环,例如回调中执行
obj.count++等修改自身依赖值的操作)然后是一个
try ... finally的语句块1
2
3
4
5try{
...
} finally {
...
}该块不是为了捕获错误,而是为了在当前副作用回调执行后进行一些操作
例如:
1
2
3
4
5
6
7
8
9
10function fn() {
try {
console.log(1);
return (() => console.log(2))();
} finally {
console.log(3);
}
}
fn(); // 1 2 3首先是
try:记录当前副作用的父副作用为上一个
activeEffect,以此形成副作用链1
this.parent = activeEffect;
把
activeEffect指向自身;并且把shouldTrack设置为true,表示当前副作用需要追踪变化1
2activeEffect = this;
shouldTrack = true;追踪操作符左移:
1
trackOpBit = 1 << ++effectTrackDepth;
全局变量
effectTrackDepth表示副作用追踪深度,初始值为0这块基础差的同学去补一下基础,几个小版本之前的实现没那么复杂,有个老外用位操作发现能优化性能,于是提了个 PR ,导致逻辑有些复杂,我就举个例子:
1二进制表示为00011 << 1表示0001左移一位,变成0010判断追踪深度是否越界:
全局变量
maxMarkerBits为常量,值为301
2
3
4
5if (effectTrackDepth <= maxMarkerBits) {
initDepMarkers(this);
} else {
cleanupEffect(this);
}如果没越界,则标记当前副作用的依赖为已追踪状态
如果越界了,则清理当前副作用的所有依赖
最后返回回调执行结果:
1
return this.fn();
然后是
finally:大多是为了嵌套的副作用的递归调用进行一些回溯的操作
回溯依赖的标记:
1
2
3if (effectTrackDepth <= maxMarkerBits) {
finalizeDepMarkers(this);
}如果越界了就没必要了,下次执行就会被清理
回溯操作符:
1
trackOpBit = 1 << --effectTrackDepth;
回溯当前副作用:
1
activeEffect = this.parent;
回溯副作用是否应该追踪的标记:
1
shouldTrack = lastShouldTrack;
断开副作用链:
1
this.parent = undefined;
如果需要延迟停止的话则停止当前副作用:
1
2
3if (this.deferStop) {
this.stop();
}
stop
这个方法逻辑很简单:
1 | stop() { |
如果当前副作用正在执行,则设置 deferStop 为 true,会在 run 方法的 finally 中延迟再次调用 stop;
会在回调 fn 中调用了 stop 时发生该情况
如果不是,并且副作用是启动的,则清空当前副作用所有依赖,并且执行 onStop 钩子函数,最后把 active 设为 false
track & trigger
上一节 ProxyHandlers 中,get 里提到的追踪 xxx和 set 里提到的触发 xxx 操作,分别调用 track 和 trigger 方法
这两个方法都围绕着一个副作用依赖表进行操作,该依赖表关系图如下:

targetMap的key存放原始对象,value存放该对象上属性的依赖映射表depsMapdepsMap的key存放对象上的属性名,value存放依赖该属性变化的副作用dep集合dep是一个Set,里面存放的是所有依赖该属性的副作用,该合集上拓展了两个字段w和n;w表示该依赖合集是否被追踪过,n表示该依赖是否是新的;
都通过位运算判断,初始值都为0
track
track 方法主要作用是根据需要追踪的原始对象和键,构建依赖表本身
接收三个参数,分别是 target 目标对象、type 操作类型、key 属性名:
1 | export function track(target: object, type: TrackOpTypes, key: unknown) { |
内部的实现也十分简单:
首先判断是否应该追踪:
1
2
3if (shouldTrack && activeEffect) {
...
}shouldTrack需要为true并且存在正在执行的副作用activeEffect否则直接结束函数
获得原始对象的属性依赖表:
1
let depsMap = targetMap.get(target);
如果不存在,则新建一个
1
2
3if (!depsMap) {
targetMap.set(target, (depsMap = new Map()));
}获得属性依赖表上依赖该属性的副作用合集:
1
let dep = depsMap.get(key);
如果不存在,则新建一个
1
2
3if (!dep) {
depsMap.set(key, (dep = createDep()));
}构建开发环境下的提示信息,最后调用
trackEffect:1
2
3
4
5const eventInfo = __DEV__
? { effect: activeEffect, target, type, key }
: undefined;
trackEffects(dep, eventInfo);
trackEffects
该方法用于将副作用添加到合集中
接收两个参数,dep 和 debuggerEventExtraInfo,dep 就是副作用依赖合集,后者则是用于开发环境的 debug 提示:
1 | export function trackEffects( |
然后看内部的实现:
首先定义一个内部的
shouldTrack:1
let shouldTrack = false;
默认值为
false接着判断下次触发副作用的模式:
1
2
3
4
5if (effectTrackDepth <= maxMarkerBits) {
...
} else {
...
}前面
run方法中,如果深度溢出会清空副作用,所以追踪的时候也有两种处理没溢出时
1
2
3
4if (!newTracked(dep)) {
dep.n |= trackOpBit; // set newly tracked
shouldTrack = !wasTracked(dep);
}如果不是新追踪,则将其标识成新追踪
并且该依赖没追踪过才需要追踪
溢出时
1
2
3
4else {
// Full cleanup mode.
shouldTrack = !dep.has(activeEffect!);
}没有该当前副作用才需要追踪
最后如果需要追踪:
1
2
3
4
5
6
7
8
9
10if (shouldTrack) {
dep.add(activeEffect!);
activeEffect!.deps.push(dep);
if (__DEV__ && activeEffect!.onTrack) {
activeEffect!.onTrack({
effect: activeEffect!,
...debuggerEventExtraInfo!,
});
}
}将当前调用的副作用加入
dep,并且在当前副作用的deps上记录副作用自身所属的依赖集合
trigger
trigger 会在对象上属性被修改时调用,用于根据操作类型构建需要调用的副作用集合
接收六个参数:
1 | export function trigger( |
target操作的原始对象type操作类型key操作的属性newValue属性的新值oldValue属性的旧值oldTarget操作前的对象拷贝,用于 debug,不重要
接着看实现:
获得原始对象的属性依赖表:
1
const depsMap = targetMap.get(target);
如果不存在属性依赖表:
1
2
3
4if (!depsMap) {
// never been tracked
return;
}说明该对象没有被追踪过,直接返回
定义一个
deps数组用于存放需要用到的副作用集合:1
let deps: (Dep | undefined)[] = [];
当操作为
CLEAR类型时:即
map.clear(),set.clear()操作1
2
3
4
5if (type === TriggerOpTypes.CLEAR) {
// collection being cleared
// trigger all effects for target
deps = [...depsMap.values()];
}清理的时候会影响所有属性,所以将所有属性的副作用集合放进
deps当修改目标为数组长度时:
1
2
3
4
5
6
7else if (key === 'length' && isArray(target)) {
depsMap.forEach((dep, key) => {
if (key === 'length' || key >= (newValue as number)) {
deps.push(dep);
}
});
}将依赖
length属性本身的副作用和溢出新长度的元素的副作用放进deps普通情况:
为
else { ... }代码块中的实现,有些多,拆开说如果
key非未定义:1
2
3if (key !== void 0) {
deps.push(depsMap.get(key));
}则从属性依赖表上将该
key上的副作用集合加入deps这边暂时只有调用
map/set.clear()方法时key会未定义跟
void 0做比较可以减少代码体积,并且 ES5 环境下undefined可能会被污染,例如var undefined = 233是可行的…下面开始判断操作类型
如果为增加操作:
map.set(k, v),set.add(v)和数组的push,unshift都会触发该操作1
2
3case TriggerOpTypes.ADD:
...
break;如果不是数组:
也就是为集合(
Map,Set)和普通对象的情况下1
2
3
4
5
6if (!isArray(target)) {
deps.push(depsMap.get(ITERATE_KEY));
if (isMap(target)) {
deps.push(depsMap.get(MAP_KEY_ITERATE_KEY));
}
}需要把依赖迭代器的副作用添加入
deps如果是
Map类型还需要额外添加其特殊的迭代器标识上的副作用如果是数组且
key为数字:也就是直接通过下标给数组增加元素的情况
1
2
3
4else if (isIntegerKey(key)) {
// new index added to array -> length changes
deps.push(depsMap.get('length'));
}这种行为会影响数组的长度,所以要添加依赖
length属性的副作用到deps
如果为删除操作:
delete语句和map/set.delete(v/k)都会触发1
2
3
4
5
6
7
8case TriggerOpTypes.DELETE:
if (!isArray(target)) {
deps.push(depsMap.get(ITERATE_KEY));
if (isMap(target)) {
deps.push(depsMap.get(MAP_KEY_ITERATE_KEY));
}
}
break;不处理数组,因为数组的删除操作不会影响长度而且数组没有迭代器,并且追踪下标上元素的副作用在一开始就添加过了
然后剩余操作跟上面是一样的
如果为修改操作:
map.set(k, v),obj.a = x都会触发该操作但是因为只有
Map有迭代器,所以只有Map才需要额外处理1
2
3
4
5case TriggerOpTypes.SET:
if (isMap(target)) {
deps.push(depsMap.get(ITERATE_KEY));
}
break;直接把依赖迭代器变化的副作用推入
deps-
这里只是一些性能优化
如果
deps里面只有一个元素:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15if (deps.length === 1) {
if (deps[0]) {
if (__DEV__) {
triggerLogCalling(
'deps length === 1 && deps[0]',
'deps[0]:',
deps[0]
);
triggerEffects(deps[0], eventInfo);
} else {
triggerEffects(deps[0]);
}
}
}直接向
triggerEffects传入第一个元素如果不止一个:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15else {
const effects: ReactiveEffect[] = []
for (const dep of deps) {
if (dep) {
effects.push(...dep)
}
}
if (__DEV__) {
triggerLogCalling('deps length !== 1', 'deps:', deps, 'effects:', effects)
triggerEffects(createDep(effects), eventInfo)
} else {
triggerEffects(createDep(effects))
}
}拍平到一个数组
effects然后再传给triggerEffects
这边的
if (__DEV__) { ... }没啥好说的,只是增加了方便 debug 的信息而已
triggerEffects
遍历所有副作用并执行
接收两个参数,dep 依赖集合和 debug 信息:
1 | export function triggerEffects( |
实现也很简单:
拍平
dep到一个数组中:1
2// spread into array for stabilization
const effects = isArray(dep) ? dep : [...dep];先执行计算属性
computed使用的副作用再取执行别的:1
2
3
4
5
6
7
8
9
10for (const effect of effects) {
if (effect.computed) {
triggerEffect(effect, debuggerEventExtraInfo);
}
}
for (const effect of effects) {
if (!effect.computed) {
triggerEffect(effect, debuggerEventExtraInfo);
}
}这样是为了保证用户自己定义的
watchEffect,watch等副作用能拿到最新的计算属性最后是
triggerEffect方法:接收两个参数,
effect副作用和 debug 信息:1
2
3
4
5
6function triggerEffect(
effect: ReactiveEffect,
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
...
}副作用不能为当前调用的副作用,或者该副作用允许递归:
1
2
3if (effect !== activeEffect || effect.allowRecurse) {
...
}下面是
if块内代码优先执行副作用上的
scheduler,否则执行其run:1
2
3
4
5if (effect.scheduler) {
effect.scheduler();
} else {
effect.run();
}
副作用流程
例如有下面的代码
1 | const obj = reactive({ count: 1 }); |
从 effect 开始看
立刻执行一次
effect的副作用回调effect.run()effect.run()将自身设置为当前副作用activeEffect = this执行副作用回调
() => console.log(obj.count)执行回调时读取了
obj.count,触发track追踪操作track把activeEffect添加到位于reactiveMap.get(obj).get('count')的副作用合集中接着执行
obj.count++触发了trigger触发操作trigger遍历reactiveMap.get(obj).get('count')中的所有副作用并执行,这边执行的是effect.run(),重复一次2-5步





