【Vue3源码学习】— CH2.6 effect.ts:详解


在Vue 3的响应式系统中, effect.ts是一个核心文件,它负责实现副作用函数( ReactiveEffect)的注册、追踪和触发更新机制。这个机制是Vue 3自动响应式更新的基石,允许我们构建高效且响应式的用户界面。以下是对 effect.ts文件关键部分的详细解析。

1. 理解activeEffect

在之前的章节中,我们看到track函数建立依赖,即将当前访问的属性与activeEffect建立依赖关系。然而,直接从effect.ts里的源码看起来,activeEffect的含义可能不是很明显。因此,在学习这部分源码时,首先需要清楚activeEffect的概念。

1.1 定义

activeEffect是一个全局变量,用于存储当前正在执行的ReactiveEffect实例。当Vue的响应式系统执行一个副作用函数时,它会把这个副作用函数对应的ReactiveEffect实例设置为activeEffect,这样在副作用函数执行期间,任何响应式数据的访问都能通过activeEffect关联起来,实现依赖追踪。

1.2 通过一个例子来说明这个过程

import { reactive, effect } from 'vue';//使用reactive创建一个响应式状态state
const state = reactive({ count: 0 });//通过effect注册一个副作用函数,这个函数简单地打印出state.count的值
effect(() => {console.log(state.count);
});//修改state.count的值时,希望上面注册的副作用函数能被重新执行,从而打印出新的count值
state.count++;

如何实现依赖追踪:

a. 副作用函数的初始化

调用effect()函数时,effect函数内部会创建一个ReactiveEffect实例来封装这个副作用函数,并立即执行副作用函数。

b. 执行副作用函数前

在执行这个副作用函数之前,effect函数会将这个ReactiveEffect实例设置为activeEffect,表示当前正在执行的副作用。

c. 访问state.count

副作用函数内部访问了state.count,触发state的get拦截器。

d. get拦截器中的track调用

get拦截器调用track函数,track检查activeEffect是否存在,从而将state.count与副作用函数关联。

e. 修改state.count时的set拦截器

修改state.count触发state的set拦截器,进而触发trigger函数。

f. trigger函数中的依赖重新执行

trigger根据建立的依赖关系找到所有依赖于state.count的副作用函数并重新执行,包括我们的示例副作用函数。

1.3 实战应用

改造我们之前的vue项目里的代码,来看一下实际使用:
在页面初始化时,effect函数就会立即执行一次副作用函数,输出0
在这里插入图片描述

执行点击事件,state.count值变化,会触发副作用函数,输出1
在这里插入图片描述

1.4 activeEffect的作用:

全局追踪: activeEffect允许响应式系统知道当前执行的副作用函数,是自动依赖追踪的关键。
依赖收集: 当响应式数据被访问时,通过activeEffect将数据与副作用函数关联起来。
更新触发: 响应式数据变化时,通过收集的依赖信息找到并执行相关副作用函数。

2. 理解ReactiveEffect

2.1 定义

ReactiveEffect类封装副作用函数及其行为,代理副作用函数的执行来跟踪依赖,并在依赖数据变化时触发更新。

2.2 源码解析

export class ReactiveEffect<T = any> {//表示这个副作用是否处于活跃状态。如果为false,则副作用不会再响应依赖数据的变化。active = true//存储了这个副作用所依赖的所有数据的集合(Dep类型)。每个Dep代表一个响应式数据的依赖关系。deps: Dep[] = []/*** 某些属性或方法可以在ReactiveEffect类的实例被创建后添加或修改,computed属性就是一个例子。* 可选属性,这个属性用于指示副作用是否与计算属性相关联,可以在创建ReactiveEffect实例之后附加上去* @internal - @internal标记意味着这部分内容主要供框架内部使用,并非设计为库或框架的公开API的一部分*/computed?: ComputedRefImpl<T>/*** @internal* 可选属性,表示是否允许这个副作用递归地调用自身*/allowRecurse?: boolean//可选回调函数,当这个副作用被停止时调用onStop?: () => void// dev only:仅在开发模式下使用的回调函数,用于调试跟踪onTrack?: (event: DebuggerEvent) => void// dev only:仅在开发模式下使用的回调函数,用于触发更新onTrigger?: (event: DebuggerEvent) => void//_dirtyLevel等内部属性: 用于内部状态管理,比如判断副作用的脏检查级别等/*** @internal*/_dirtyLevel = DirtyLevels.Dirty/*** @internal*/_trackId = 0/*** @internal*/_runnings = 0/*** @internal*/_shouldSchedule = false/*** @internal*/_depsLength = 0/*** 构造函数* fn:副作用函数* trigger:触发函数(内部使用,通常为NOOP,即空操作)* scheduler:可选的调度器函数* scope:可选的作用域*/constructor(public fn: () => T,public trigger: () => void,public scheduler?: EffectScheduler,scope?: EffectScope,) {recordEffectScope(this, scope)}/*** dirty属性的get方法在ReactiveEffect类中是用来确定副作用函数是否需要被重新执行的。这个机制主要用于优化计算属性和其他依赖缓存的场景*/public get dirty() {//检查_dirtyLevel:检查当前副作用的_dirtyLevel(脏检查级别)if (this._dirtyLevel === DirtyLevels.MaybeDirty_ComputedSideEffect ||this._dirtyLevel === DirtyLevels.MaybeDirty) {//查询脏数据:为了确定是否真的需要重新计算,会将_dirtyLevel设置为QueryingDirty,this._dirtyLevel = DirtyLevels.QueryingDirty//并暂时停止依赖追踪(以避免在这个过程中不必要地收集新的依赖)pauseTracking()/*** 检查计算属性:遍历所有已注册的依赖(这些依赖代表副作用函数所依赖的数据)。*/for (let i = 0; i < this._depsLength; i++) {const dep = this.deps[i]//如果其中的依赖是计算属性(dep.computed),则会尝试触发这些计算属性的重新计算(通过triggerComputed函数)。if (dep.computed) {triggerComputed(dep.computed)//如果在这个过程中发现_dirtyLevel变为Dirty,表示确实有数据变化,需要重新计算副作用函数,那么循环就会提前结束if (this._dirtyLevel >= DirtyLevels.Dirty) {break}}}/*** 重置脏检查级别:如果遍历完所有依赖后_dirtyLevel仍然是QueryingDirty,* 表示没有发现需要更新的数据,那么将_dirtyLevel设置为NotDirty,意味着不需要重新执行副作用函数。*/if (this._dirtyLevel === DirtyLevels.QueryingDirty) {this._dirtyLevel = DirtyLevels.NotDirty}//最后,恢复依赖追踪resetTracking()}//返回值:最后,通过检查_dirtyLevel是否大于等于Dirty来决定是否标记为"脏",如果是,则表示需要重新执行副作用函数return this._dirtyLevel >= DirtyLevels.Dirty}public set dirty(v) {this._dirtyLevel = v ? DirtyLevels.Dirty : DirtyLevels.NotDirty}/*** 执行副作用函数* 在执行之前,会设置全局的activeEffect为当前实例,以便在副作用函数执行期间能够收集依赖。* 执行完成后,恢复activeEffect和shouldTrack的状态。* 如果副作用当前不处于活跃状态,直接执行副作用函数而不进行依赖收集*/run() {this._dirtyLevel = DirtyLevels.NotDirtyif (!this.active) {return this.fn()}let lastShouldTrack = shouldTracklet lastEffect = activeEffecttry {//shouldTrack设置为true,允许依赖收集shouldTrack = true/*** this引用的是ReactiveEffect类的当前实例* 将全局的activeEffect变量设置为当前的副作用实例*/activeEffect = this/*** _runnings属性记录了当前副作用函数被执行的次数* 这个计数器主要用于内部管理,确保副作用的正确执行和清理*/this._runnings++preCleanupEffect(this)return this.fn()} finally {postCleanupEffect(this)this._runnings--activeEffect = lastEffectshouldTrack = lastShouldTrack}}/*** 停止这个副作用响应其依赖数据的变化。在停止前后,会执行一些清理操作,并调用onStop回调(如果有的话)*/stop() {if (this.active) {preCleanupEffect(this)postCleanupEffect(this)this.onStop?.()this.active = false}}
}

2.3 重要属性解释

active: 控制副作用是否响应依赖数据的变化。
deps: 存储副作用所依赖的数据集合。
dirty: 脏检查机制优化计算属性更新。

3. 理解effect函数

3.1 定义

在Vue的响应式系统中,effect函数用于注册一个副作用函数(effect function),这个副作用函数可以自动响应其内部使用的响应式数据的变化。简单来说,就是当我们使用reactive或ref创建的响应式数据在这个副作用函数内被访问时,Vue会记住这个访问行为,并在数据变化时重新执行这个副作用函数。

3.2 源码解析

/*** fn:要注册为副作用的函数,当响应式数据变化时,这个函数会被重新执行。* options:可选参数,用于控制副作用的行为。常见的选项包括:*    - lazy:如果为true,则副作用函数不会立即执行,直到手动调用返回的runner函数。*    - scheduler:自定义的调度函数,用于控制副作用函数的重新执行时机。*    - onStop:当调用stop函数停止副作用时执行的回调函数。*/
export function effect<T = any>(fn: () => T,options?: ReactiveEffectOptions,
): ReactiveEffectRunner {// 如果fn已经是一个effect函数,则获取其原始函数进行重新包装if ((fn as ReactiveEffectRunner).effect instanceof ReactiveEffect) {fn = (fn as ReactiveEffectRunner).effect.fn}// 创建一个ReactiveEffect实例,封装副作用函数fnconst _effect = new ReactiveEffect(fn, NOOP, () => {if (_effect.dirty) {_effect.run()}})// 应用传入的options配置到_effect实例上if (options) {extend(_effect, options)if (options.scope) recordEffectScope(_effect, options.scope)}// 如果不是懒执行(lazy为false),则立即执行一次副作用函数if (!options || !options.lazy) {_effect.run()}// 返回一个可以控制副作用执行的runner函数,并附加effect实例const runner = _effect.run.bind(_effect) as ReactiveEffectRunnerrunner.effect = _effectreturn runner
}

3.3 工作原理

3.3.1 封装副作用函数

通过创建一个ReactiveEffect实例来封装传入的fn函数。这个封装包括对副作用函数的执行上下文管理,以及依赖追踪和触发逻辑的处理。

3.3.2 自动执行和响应

默认情况下,封装的副作用函数会立即执行一次。在执行过程中,ReactiveEffect会设置activeEffect指向当前的副作用实例,这样任何被访问的响应式数据都会将当前的副作用函数收集为依赖。当响应式数据变化时,所有收集的依赖会被重新执行,即重新执行副作用函数。

3.3.3 控制执行

effect函数返回一个runner函数,通过这个runner函数可以手动控制副作用的执行,特别是在配置了lazy选项时。同时,runner函数上附带的effect属性允许直接访问到内部的ReactiveEffect实例,进一步控制或查询副作用的状态。

3.4 runner解析

const runner = _effect.run.bind(_effect) as ReactiveEffectRunner
runner.effect = _effect

这段代码的目的是创建一个可以被直接调用的函数(runner),用来控制副作用(effect)的执行,并且让这个runner函数具有一些额外的属性和能力。下面详细解释这段代码的含义和作用:

简化API:
通过返回一个函数(runner),使用者可以直接执行这个函数来触发副作用,而不需要从_effect实例中调用run方法。这样的API更加简洁直观。

绑定上下文:_effect.run.bind(_effect):

使用.bind(_effect)是为了确保在run方法内部,this关键字指向的是_effect实例本身,而不是其他的上下文。这样,无论runner函数在哪里被调用,它内部通过this访问的都是正确的ReactiveEffect实例。

类型安全:as ReactiveEffectRunner:
这是TypeScript的类型断言,用来指明runner函数是ReactiveEffectRunner类型。ReactiveEffectRunner类型除了是一个可调用的函数之外,还额外附带了一个effect属性。这样做的目的是为了让TypeScript的类型系统知道runner不仅仅是一个普通的函数,还是一个特殊的对象,拥有effect属性。

保留引用:runner.effect = _effect:
这行代码给runner函数对象添加了一个名为effect的属性,并将这个属性的值设置为_effect实例。这样做的目的是让外部代码能够通过runner函数直接访问到内部的ReactiveEffect实例。这在某些场景下非常有用,比如需要停止或查询副作用的状态时,可以通过runner.effect来操作。

综上所述,这段代码创建了一个runner函数,这个函数不仅可以被调用来执行副作用,还允许外部代码通过runner.effect直接访问和控制副作用实例。这是Vue响应式系统灵活且强大的设计之一。

4. 小结

再看我们1.2的示例来回顾一下,这里的每一步都是响应式系统核心机制的重要组成部分:

4.1 副作用函数的初始化

使用effect(fn)时,Vue会创建一个ReactiveEffect实例,这个实例包装了用户定义的副作用函数fn。这时,全局的activeEffect被设置为这个实例,并且shouldTrack变为true,允许依赖收集。

4.2 依赖收集(Tracking)

在副作用函数执行过程中,任何被访问的响应式对象属性(例如state.count)的访问都会被相应对象的get拦截器捕获。get拦截器会调用track函数,将当前的activeEffect(副作用函数)与这个属性关联起来,并在targetMap中记录这个依赖关系。

4.3 触发更新(Triggering)

当响应式对象的属性被修改时(如state.count被赋予一个新值),这个操作会被属性所属对象的set拦截器捕获。set拦截器随后调用trigger函数,查找所有依赖于被修改属性的副作用函数,并触发它们重新执行。

4.4 重新执行副作用函数

trigger函数根据在targetMap中记录的依赖关系找到所有依赖的副作用函数,并重新执行它们。这样,任何基于这个响应式数据的计算或UI更新都会被自动进行。

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/578931.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【数据结构】堆、堆排序(包你学会的)

文章目录 前言堆&#xff08;Heap&#xff09;1、堆的概念及结构2、堆的分类2.1、小堆的结构2.2、大堆的结构2.3、找到规律并证明 3、堆的实现&#xff08;小堆&#xff09;3.1、堆的结构以及接口3.2、初始化、销毁3.3、交换父子结点&#xff08;后续需要&#xff09;3.4、插入…

代码随想录算法训练营第二十四天| 理论基础,77. 组合

题目与题解 参考资料&#xff1a;回溯法理论基础 带你学透回溯算法&#xff08;理论篇&#xff09;| 回溯法精讲&#xff01;_哔哩哔哩_bilibili 77. 组合 题目链接&#xff1a;​​​​​​​​​​​​​​77. 组合 代码随想录题解&#xff1a;77. 组合 视频讲解&#xff…

c语言中的联合体和枚举

这篇文章总结一下c语言中的联合体和枚举。看看这两个东西到底是什么。大家一起学习。 文章目录 一、联合体1.联合体类型的声明。2.联合体的大小。3.相同成员的结构体和联合体对比4.联合体大小的计算。 二、枚举类型1.枚举类型的声明。2.枚举类型的优点。枚举类型的使用。 一、联…

C++王牌结构hash:哈希表开散列(哈希桶)的实现与应用

目录 一、开散列的概念 1.1开散列与闭散列比较 二、开散列/哈希桶的实现 2.1开散列实现 哈希函数的模板构造 哈希表节点构造 开散列增容 插入数据 2.2代码实现 一、开散列的概念 开散列法又叫链地址法(开链法)&#xff0c;首先对关键码集合用散列函数计算散列地址&…

一文教你如何轻松领取腾讯云优惠券

腾讯云作为国内领先的云计算服务商&#xff0c;为用户提供了丰富的云产品和服务。为了让更多用户享受到腾讯云服务的优质体验&#xff0c;腾讯云推出了各种优惠券&#xff0c;让用户在购买云服务时能够获得更多实惠。本文将为大家详细介绍如何轻松领取腾讯云优惠券&#xff0c;…

智慧公厕,为智慧城市建设注入了新的活力

随着智慧城市的快速发展&#xff0c;公共厕所不再是简单的功能设施&#xff0c;而是成为了提升城市形象、改善民生服务的重要一环。智慧公厕作为新形态的公共厕所&#xff0c;通过精准监测公厕内部的人体活动状态、人体存在状态、空气质量情况、环境变化情况、设施设备运行状态…

使用PopLDdecay软件绘制LD衰减图

前记 PopLDdecay是一款用于进行种群遗传学和关联分析的软件。它可以在全基因组水平上进行基因型数据的相关性和衰减分析&#xff0c;帮助研究人员探索种群间的遗传差异和突变选择的模式。 使用PopLDdecay可以实现以下功能&#xff1a; 遗传距离的计算&#xff1a;可以计算遗…

bugku-web-eval

页面源码 <code><span style"color: #000000"> <span style"color: #0000BB"><?php <br /> </span><span style"color: #007700">include </span><span style"color: #DD0000"&…

37-巩固练习(一)

37-1 if语句等 1、问&#xff1a;输出结果 int main() {int i 0;for (i 0; i < 10; i){if (i 5){printf("%d\n", i);}return 0;} } 答&#xff1a;一直输出5&#xff0c;死循环 解析&#xff1a;i5是赋值语句&#xff0c;不是判断语句&#xff0c;每一次循…

类与对象中C++

加油&#xff01;&#xff01;&#xff01; 文章目录 前言 一、类的6个默认成员函数 ​编辑 二、构造函数 1.概念 三、析构函数 1.概念 2.特性 四、拷贝构造函数 1.概念 2.特征 拷贝构造函数典型调用场景 五、赋值运算符重载 1.运算符重载 2.赋值运算符重载 赋值运算符重载格式…

gitee规范团队 代码提交

1.团队开会规范 使用 插件 &#xff1a; git Commit Message Helper 插件进行代码提交前规范 2.gitee代码仓库断控制&#xff0c;上面只是规范了程序员开发端&#xff1b;但是gitee也要管理控制&#xff1b;正则根据每个公司的不同来进行。

基于Springboot+vue的宠物销售商城网站

摘 要 随着科学技术的飞速发展&#xff0c;社会的方方面面、各行各业都在努力与现代的先进技术接轨&#xff0c;通过科技手段来提高自身的优势&#xff0c;宠物销售商城当然也不能排除在外。宠物销售商城是以实际运用为开发背景&#xff0c;运用软件工程原理和开发方法&#x…