Vue3源码reactivity响应式篇之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);}
}
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方法执行完后,恢复activeSubactiveSub = prevSub;}}
}
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原来的activeLinklink.dep.activeLink = link; // 将当前依赖链接设为dep的activeLink}
}
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的订阅链表中移除linkremoveDep(link); // 从副作用的依赖链表中移除该link}else{// 若version已更新即不为-1,则更新head指针head = link;}// 恢复Dep的activeLinklink.dep.activeLink = link.prevActiveLink;link.prevActiveLink = undefined; // 清空暂存值link = prev; //继续遍历前一个link}// 更新副作用的依赖链表为过滤后的结果sub.deps = head;sub.depsTail = tail;
}
cleanupDeps
是通过removeSub
(从Dep
移除)和removeDep
(从副作用的依赖链表移除)进行双向清理彻底切断无效依赖的关联。而后进行链表重构,确保下次更新仅通知有效依赖。
removeSub
removeSub
方法用于从Dep
的订阅链表中移除指定link
链接,确保Dep
触发更新时不再通知已无效的订阅者。
removeSub
的源码实现如下:
function removeSub(link,soft = false){const {dep,prevSub,nextSub} = link; // link关联的dep、前后订阅链接// 调整链表指针,移除当前linkif(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); }
}
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;}
}
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标记,则直接返回trueif(sub._dirty){return true; }return false;
}