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

2024-03-30 12:12

本文主要是介绍【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更新都会被自动进行。

在这里插入图片描述

这篇关于【Vue3源码学习】— CH2.6 effect.ts:详解的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/861461

相关文章

SpringIoC与SpringDI详解

《SpringIoC与SpringDI详解》本文介绍了Spring框架中的IoC(控制反转)和DI(依赖注入)概念,以及如何在Spring中使用这些概念来管理对象和依赖关系,感兴趣的朋友一起看看吧... 目录一、IoC与DI1.1 IoC1.2 DI二、IoC与DI的使用三、IoC详解3.1 Bean的存储

Java进阶学习之如何开启远程调式

《Java进阶学习之如何开启远程调式》Java开发中的远程调试是一项至关重要的技能,特别是在处理生产环境的问题或者协作开发时,:本文主要介绍Java进阶学习之如何开启远程调式的相关资料,需要的朋友... 目录概述Java远程调试的开启与底层原理开启Java远程调试底层原理JVM参数总结&nbsMbKKXJx

Spring Cloud之注册中心Nacos的使用详解

《SpringCloud之注册中心Nacos的使用详解》本文介绍SpringCloudAlibaba中的Nacos组件,对比了Nacos与Eureka的区别,展示了如何在项目中引入SpringClo... 目录Naacos服务注册/服务发现引⼊Spring Cloud Alibaba依赖引入Naco编程s依

C语言中的浮点数存储详解

《C语言中的浮点数存储详解》:本文主要介绍C语言中的浮点数存储详解,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录1、首先明确一个概念2、接下来,讲解C语言中浮点型数存储的规则2.1、可以将上述公式分为两部分来看2.2、问:十进制小数0.5该如何存储?2.3 浮点

大数据spark3.5安装部署之local模式详解

《大数据spark3.5安装部署之local模式详解》本文介绍了如何在本地模式下安装和配置Spark,并展示了如何使用SparkShell进行基本的数据处理操作,同时,还介绍了如何通过Spark-su... 目录下载上传解压配置jdk解压配置环境变量启动查看交互操作命令行提交应用spark,一个数据处理框架

MySQL中COALESCE函数示例详解

《MySQL中COALESCE函数示例详解》COALESCE是一个功能强大且常用的SQL函数,主要用来处理NULL值和实现灵活的值选择策略,能够使查询逻辑更清晰、简洁,:本文主要介绍MySQL中C... 目录语法示例1. 替换 NULL 值2. 用于字段默认值3. 多列优先级4. 结合聚合函数注意事项总结C

Java实现数据库图片上传功能详解

《Java实现数据库图片上传功能详解》这篇文章主要为大家详细介绍了如何使用Java实现数据库图片上传功能,包含从数据库拿图片传递前端渲染,感兴趣的小伙伴可以跟随小编一起学习一下... 目录1、前言2、数据库搭建&nbsChina编程p; 3、后端实现将图片存储进数据库4、后端实现从数据库取出图片给前端5、前端拿到

Windows命令之tasklist命令用法详解(Windows查看进程)

《Windows命令之tasklist命令用法详解(Windows查看进程)》tasklist命令显示本地计算机或远程计算机上当前正在运行的进程列表,命令结合筛选器一起使用,可以按照我们的需求进行过滤... 目录命令帮助1、基本使用2、执行原理2.1、tasklist命令无法使用3、筛选器3.1、根据PID

Nginx实现前端灰度发布

《Nginx实现前端灰度发布》灰度发布是一种重要的策略,它允许我们在不影响所有用户的情况下,逐步推出新功能或更新,通过灰度发布,我们可以测试新版本的稳定性和性能,下面就来介绍一下前端灰度发布的使用,感... 目录前言一、基于权重的流量分配二、基于 Cookie 的分流三、基于请求头的分流四、基于请求参数的分

MySql中的数据库连接池详解

《MySql中的数据库连接池详解》:本文主要介绍MySql中的数据库连接池方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录mysql数据库连接池1、概念2、为什么会出现数据库连接池3、原理4、数据库连接池的提供商5、DataSource数据源6、DBCP7、C