ReactiveEffect类介绍
# 概览
在vue3的响应式系统设计中,ReactiveEffect
类是副作用管理的核心类,负责封装所有需要响应式触发的函数(如组件渲染函数,watch
回调、计算属性的求值函数等),并实现副作用的依赖收集、触发执行、暂停/恢复、停止等完整生命周期管理。
# 源码分析
ReactiveEffect
的本质就是对副作用函数的封装,它解决如下三个核心问题:
- 依赖收集:执行副作用函数时,自动记录它所依赖的响应式数据
- 触发更新:当依赖的数据变化时,自动重新执行副作用函数(或通过调度器控制执行时机)
- 生命周期管理:支持暂停、恢复、停止副作用,以及清理依赖关系。
ReactiveEffect
的源码实现如下:
class ReactiveEffect {
constructor(fn) {
this.fn = fn; // 副作用函数本体
// 依赖管理相关:存储当前副作用依赖的dep(即双向链表结构)
this.deps = []; // 依赖链表的头部
this.depsTail = null; // 依赖链表的尾部
this.flags = 1 | 4; // 标志位, 1:激活 4:需要追踪依赖
this.next = null; // 批量更新相关:链表指针用于批量队列中串联多个副作用
// 回调与调度器
this.cleanup = null; //清理函数(执行前触发,用于清除旧副作用)
this.scheduler = null; // 调度器(自定义副作用的执行时机,如watch的flush配置)
this.onStop = null;// 停止时的回调
this.onTrack = null;// 依赖收集时的调试回调
this.onTrigger = null;// 触发更新时的调试回调
// 关联到当前活跃的effectScope副作用域中
if (activeEffectScope && activeEffect.active) {
activeEffectScope.effects.push(this);
}
}
// 暂停副作用
pause() {
this.flags |= 64; // 标记为暂停状态
}
// 恢复副作用
resume() {
if (this.flags & 64) { //若当前副作用已暂停
this.flags &= -65; // 清除暂停标记
// 若暂停期间有触发更新,则从暂停队列中先清除,再触发更新
if (pausedQueueEffects.has(this)) {
pausedQueueEffects.delete(this);
this.trigger();
}
}
}
// 批量更新通知
notify() {
// 若副作用正在执行,其不允许递归,则不处理
if (this.flags & 2 && !(this.flags & 32)) {
return;
}
// 若副作用未调度,则调用`batch`方法
if (!(this.flags & 8)) {
batch(this);
}
}
// 执行副作用
run() {
// 若副作用未激活,则直接执行副作用函数,但不追踪依赖。
if (!(this.flags & 1)) {
return this.fn()
}
// 标记状态为正在执行
this.flags |= 2;
// 清理旧依赖
cleanupEffect(this);
// 准备新的依赖链接
prepareDeps(this);
// 保存当前活跃的副作用和追踪状态,并切换到当前副作用
const prevEffect = activeSub;
const prevShouldTrack = shouldTrack;
activeSub = this; // 当前副作用成为活跃订阅者,在Dep.track中进行依赖收集
shouldTrack = true; //允许依赖收集
try {
return this.fn(); // 执行副作用函数,(如组件渲染,计算属性求值)
} finally {
// 清理临时依赖状态
cleanupDeps(this);
//恢复活跃订阅者以及追踪状态
activeSub = prevEffect; // 恢复上一个活跃订阅者
shouldTrack = prevShouldTrack; // 恢复上一个追踪状态
this.flags &= -3; // 清除正在执行和已暂停标记
}
}
// 停止副作用
stop() {
if (this.flags & 1) { // 若当前是激活状态
// 遍历所有依赖链接,从dep中移除当前副作用
for (let link = this.deps; link; link = link.nextDep) {
removeSub(link); // 调用removeSub从Dep的订阅者链表中删除
}
// 清空自身的依赖链表
this.deps = this.depsTail = void 0;
cleanupEffect(this);//清理残留的以来
this.onStop && this.onStop();//触发停止回调
this.flags &= -2; // 清除激活状态
}
}
// 触发执行
trigger() {
if (this.flags & 64) {//若已暂停,则加入暂停队列
pausedQueueEffects.add(this);
} else if (this.scheduler) {
// 若有自定义调度器,则使用调度器执行
this.scheduler();
} else {
//否则调用this.runIfDirty()方法检查是否需要执行
this.runIfDirty();
}
}
// 仅当副作用函数被标记为脏时,才执行
runIfDirty() {
if (isDirty(this)) {
this.run();
}
}
// 判断是否“脏”
get dirty() {
return isDirty(this);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
ReactiveEffect
的pause()
与resume()
方法就是通过改变flag
的值实现状态控制,用于暂停和恢复副作用的执行,典型场景如组件卸载时暂停渲染副作用,重新挂载时恢复。notify()
方法用于批量更新通知,当副作用依赖的数据变化时,Dep
会调用此方法,将副作用加入批量更新队列中,而不是立即执行。- 而
run
方法则是ReactiveEffect
类的核心方法,用于执行副作用函数fn
,并在执行前后处理依赖收集和状态恢复。 stop
方法则是永久停止副作用,清理所有依赖关系,确保数据变化时不再触发执行,比如组件卸载时停止渲染副作用。trigger
方法用于副作用执行,根据副作用状态决定执行方式。runIfDirty
方法用于检查副作用是否“脏”,若“脏”则执行副作用函数,否则不执行。dirty
属性用于判断副作用是否“脏”,若“脏”则返回true
,否则返回false
。
# 辅助方法
ReactiveEffect
类中有许多辅助方法,比如清理副作用cleanupEffect
,准备依赖prepareDeps
,清理依赖cleanupDeps
,移除依赖removeSub
等。
# cleanupEffect
cleanupEffect
方法用于清理副作用函数fn
的依赖关系,确保在副作用函数执行前,先清理旧的依赖关系,再准备新的依赖关系。
cleanup
该方法一般由用户或者第三方插件库自定义,vue3中不负责该方法的具体实现。
cleanupEffect
的源码如下:
function cleanupEffect(e){
const {cleanup} = e;
e.cleanup = undefined;
if(cleanup){
// 先将activeSub设为undefined,避免cleanup内部操作响应式数据
const prevSub = activeSub;
activeSub = undefined;
try{
cleanup();
} finally {
//清理cleanup方法执行完后,恢复activeSub
activeSub = prevSub;
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# prepareDeps
prepareDeps
方法在run
中初始化依赖链表状态时调用,为后续过滤无效依赖做准备,适配依赖动态变化的场景
prepareDeps
的源码实现如下:
function prepareDeps(sub){
// 遍历副作用的所有依赖链接 Link实例
for(let link = sub.deps;link;link=link.nextDep){
link.version = -1; // 标记为待验证状态 -1表示未被访问
link.prevActiveLink = link.dep.activeLink; // 暂存dep原来的activeLink
link.dep.activeLink = link; // 将当前依赖链接设为dep的activeLink
}
}
2
3
4
5
6
7
8
prepareDeps
会将所有依赖链接的版本号设为*-1*,若依赖被访问,则Dep.track
会将link.version
更新为dep.version
;若未被访问,则该依赖的版本号仍为*-1*,它会在cleanDeps
中被识别过滤出来清除。
# cleanupDeps
function cleanupDeps(sub){
let head;// 过滤后的依赖链表的头部
let tail = sub.depsTail; // 从原链表尾部开始遍历
let link = tail;
while(link){
const prev = link.prevDep; // 记录上一个链接
if(link.version === -1){
// 若 version 仍为 -1,则说明本次执行未曾访问该依赖,该依赖是无效的,需要移除
if(link === tail){
tail = prev; // 更新tail指针,尾部指向上一个链接
}
removeSub(link); // 从Dep的订阅链表中移除link
removeDep(link); // 从副作用的依赖链表中移除该link
}else{
// 若version已更新即不为-1,则更新head指针
head = link;
}
// 恢复Dep的activeLink
link.dep.activeLink = link.prevActiveLink;
link.prevActiveLink = undefined; // 清空暂存值
link = prev; //继续遍历前一个link
}
// 更新副作用的依赖链表为过滤后的结果
sub.deps = head;
sub.depsTail = tail;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
cleanupDeps
是通过removeSub
(从Dep
移除)和removeDep
(从副作用的依赖链表移除)进行双向清理彻底切断无效依赖的关联。而后进行链表重构,确保下次更新仅通知有效依赖。
# removeSub
removeSub
方法用于从Dep
的订阅链表中移除指定link
链接,确保Dep
触发更新时不再通知已无效的订阅者。
removeSub
的源码实现如下:
function removeSub(link,soft = false){
const {dep,prevSub,nextSub} = link; // link关联的dep、前后订阅链接
// 调整链表指针,移除当前link
if(prevSub){
prevSub.nextSub = nextSub; //前一个链接的nextSub指向后一个链接
link.prevSub = undefined; // 清空当前link的前指针
}
if(nextSub){
nextSub.prevSub = prevSub; // 后一个链接的prevSub指向前一个链接
link.nextSub = undefined; // 清空当前link的后指针
}
// 更新Dep的头尾指针
if(dep.subsHead === link){
dep.subsHead = nextSub; // 若link是头部,则头部更新为后一个链接
}
// 若link是尾部,则更新尾部为前一个链接
if(dep.subs === link){
dep.subs = prevSub;
// 特殊处理,若Dep关联的计算属性且没有剩余订阅者,则清理计算属性
if(!prevSub && dep.computed){
dep.computed.flags &= -5;//清除计算属性的活跃标志
// 递归移除计算属性的依赖(反向清理)
for(let l =dep,computed.deps;l;l = l.nextDep){
removeSub(l,true);
}
}
}
// 若不是软删除且Dep引用计数为0且有map映射,则从map中移除
if(!soft && !--dep.sc && dep.map){
dep.map.delete(dep.key);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
removeSub
通过调整preSub
和nextSub
指针,确保移除link
后链表依然完整,避免悬空指针问题。
# removeDep
removeDep
方法用于从Dep
的依赖链表中移除指定link
链接,确保在依赖更新时不再通知已无效的副作用函数。
removeDep
的源码实现如下:
function removeDep(link){
const {prevDep,nextDep}=link;
if(prevDep){
prevDep.nextDep = nextDep;
link.prevDep = undefined;
}
if(nextDep){
nextDep.prevDep = prevDep;
link.nextDep = undefined;
}
}
2
3
4
5
6
7
8
9
10
11
# isDirty
isDirty
方法用于检查ReactiveEffect
实例是否需要重新运行,判断条件包括依赖版本是否更新或计算属性是否脏值。
isDirty
的源码实现如下:
function isDirty(sub){
// 遍历检查所有依赖链接是否有更新
for(let link =sub.deps;link;link= link.nextDep){
// 当满足如下两个条件之一时,则说明依赖有更新
// 1. 依赖的版本与link记录的版本不一致,说明依赖已更新
// 2. 依赖关联计算属性且计算属性需刷新,且版本仍不一致
if(link.dep.version !== link.version || link.dep.computed && (refreshComputed(link.dep.computed)|| link.dep.version !== link.version)){
return true;
}
}
// 若副作用自身具有_dirty标记,则直接返回true
if(sub._dirty){
return true;
}
return false;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17