【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.rhkb.cn/news/294533.html

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

相关文章

Mysql数据备份与恢复实战

文章目录 备份类型备份内容备份工具mysqldump备份 实战案例&#xff1a;恢复误删除的表准备工作2:30完全备份完全备份后更新数据表10:00误删students表需要恢复还原的状态开始还原恢复 为什么要备份&#xff1f; 备份是为了&#xff1a;灾难恢复&#xff1a;硬件故障、软件故障…

Php_Code_challenge16

题目&#xff1a; 答案&#xff1a; 解析&#xff1a; 所以科学计数法绕过即可。

旅游管理系统|基于Springboot的旅游管理系统设计与实现(源码+数据库+文档)

旅游管理系统目录 目录 基于Springboot的旅游管理系统设计与实现 一、前言 二、系统功能设计 三、系统实现 1、用户管理 2、景点分类管理 3、景点信息管理 4、酒店信息管理 5、景点信息 6、游记分享管理 四、数据库设计 1、实体ER图 2、具体的表设计如下所示&#xf…

ZKFair 创新之旅,新阶段如何塑造财富前景

在当前区块链技术的发展中&#xff0c;Layer 2&#xff08;L2&#xff09;解决方案已成为提高区块链扩容性、降低交易成本和提升交易速度的关键技术&#xff0c;但它仍面临一些关键问题和挑战&#xff0c;例如用户体验的改进、跨链互操作性、安全性以及去中心化程度。在这些背景…

Python控制安卓模拟器——uiautomator2模块

Python控制安卓模拟器——uiautomator2模块 目录 Python控制安卓模拟器——uiautomator2模块介绍【1】安装 python【2】安装 adb1]下载[adb:[2]配置环境变量 【3】安装uiautomator2【4】连接设备(安卓模拟器)【5】u2指令控制设备常用指令 【6】安装weditor【7】元素操作元素属性…

小白的第一次sql注入实战

前言 当时最初接触安全的时候拿下的第一个shell&#xff0c;还是比较兴奋的&#xff0c;忽略一下文章写的很水。 有id尝试sql注入 找这种sql注入的站用sql检索就行了&#xff0c;但是最好挂代理用谷歌搜索&#xff0c;百度的话搜sql注入的很多被别人打过了&#xff0c;导致链…

钉钉服务端API报错 43008 参数需要multipart类型

钉钉服务端API报错 43008 参数需要multipart类型 problem 使用媒体文件上传接口&#xff0c;按照文档输入参数&#xff0c;结果返回报错 # 参数 {"access_token": "xxx""type": "image","media": "/Users/xxx/xxx/s…

hcia datacom课程学习(5):MAC地址与arp协议

1.MAC地址 1.1 含义与作用 &#xff08;1&#xff09;含义&#xff1a; mac地址也称物理地址&#xff0c;是网卡设备在数据链路层的地址&#xff0c;全世界每一块网卡的mac地址都是唯一的&#xff0c;出厂时烧录在网卡上不可更改 &#xff08;2&#xff09;作用&#xff1a…

constexpr与std::is_same_v碰撞会产生什么火花?

1. 只编译会用到的if分支 示例代码一中&#xff0c;checkType_v1和checkType_v2两个函数的区别就是if的条件里一个加了constexpr一个没加&#xff0c;加与不加从结果来看都一样&#xff0c;那在编译时和运行时各有什么区别呢&#xff1f; 示例代码一&#xff0c;test_01.cpp&…

Linux之实现Apache服务器监控、数据库定时备份及通过使用Shell脚本发送邮件

目录 一、Apache服务器监控 为什么要用到服务监控&#xff1f; 实现Apache服务器监控 二、数据库备份 为什么要用到数据库备份&#xff1f; 实现数据库备份 三、Shell脚本发送邮件 为什么要用使用Shell脚本发送邮件&#xff1f; 实现Shell脚本发送邮件 一、Apache服务器…

大模型日报20240401

大模型实时打《街霸》捉对PK&#xff0c;GPT-4居然不敌3.5&#xff0c;新型Benchmark火了 链接&#xff1a;https://news.miracleplus.com/share_link/22340 让大模型直接操纵格斗游戏《街霸》里的角色&#xff0c;捉对PK&#xff0c;谁更能打&#xff1f;GitHub上一种你没有见…

Windows下配置及使用Git+rsync构建文件同步工具

背景(了解)&#xff1a; 公司的前端项目里有bash脚本&#xff0c;开发人员在开发完&#xff0c;可以跑bash脚本自动发布部署到测试环境。但是windows是没办法直接跑bash脚本的&#xff0c;最便捷的做法就是借用git bash这个终端来跑项目里的bash脚本。但是windows的源里并没有…

conda使用记录

linux 使用conda创建新一个新的python环境过程 conda create -n recommendation_env python3.8.18 # 指定python版本 conda env list # 查看所有的环境 conda activate recommendation_env # 激活创建的新环境 pip install flask # 安装依赖 或者 pip install flask版本号 或者…

Python+requests+Pytest+logging+allure+pymysql框架详解

一、框架目录结构 1&#xff09;tools目录用来放公共方法存储&#xff0c;如发送接口以及读取测试数据的方法&#xff0c;响应断言 数据库断言 前置sql等方法&#xff1b;2&#xff09;datas目录用例存储接口用例的测试数据&#xff0c;我是用excel来存储的数据&#xff0c;文…

简单说清楚什么是SQL Injection?

最近看完了《The Pragmatic Programmer: 20th Anniversary Edition, 2nd Edition: Your Journey to Mastery》&#xff0c;在第7章&#xff1a;While You Are Coding的footnotes中&#xff0c;提到了一幅漫画&#xff1a; 这不仅用简单的方式说清楚了什么是SQL Injection&#…

顶顶通呼叫中心中间件-声音编码自适应配置方法(mod_cti基于FreeSWITCH)

顶顶通呼叫中心中间件-声音编码自适应配置方法讲解(mod_cti基于FreeSWITCH) 声音编码自适应介绍 声音编码自适应&#xff0c;通常在语音通信和音频处理领域中指的是一种能够根据信号特性和传输环境自动调整编码参数的技术。其目的是在不同的网络状况和音质要求下&#xff0c;…

uniapp开发App(一)登陆流程 判断是否登陆,是,进入首页,否,跳转到登录页

一、登陆流程 文字描述&#xff1a;用户进入App&#xff0c;之后就是判断该App是否有用户登陆过&#xff0c;如果有&#xff0c;直接进入首页&#xff0c;否则跳转到登陆页&#xff0c;登陆成功后&#xff0c;进入首页。 流程图如下&#xff1a; 二、在uniapp项目中代码实现 实…

【数据结构】AVL 树

文章目录 1. AVL 树的概念2. AVL 树节点的定义3. AVL 树的插入4. AVL 树的旋转5. AVL 树的验证6. AVL 树的删除7. AVL 树的性能 前面对 map / multimap / set / multiset 进行了简单的介绍【C】map & set&#xff0c;在其文档介绍中发现&#xff0c;这几个容器有个共同点是…

oracle19c安装-aarch64

建议 参考oracle官方文档提供的软硬件要求 https://docs.oracle.com/en/database/oracle/oracle-database/19/ladbi/operating-system-checklist-for-oracle-database-installation-on-linux.html#GUID-E5C0A90E-7750-45D9-A8BC-C7319ED934F0 建议使用OracleLinux8.6及以上操作…

RWKV_Pytorch:支持多硬件适配的开源大语言模型推理框架

亲爱的技术探索者们&#xff0c;今天我要向大家隆重推荐一个在开源社区中崭露头角的项目——RWKV_Pytorch。这是一个基于Pytorch的RWKV大语言模型推理框架&#xff0c;它不仅具备高效的原生Pytorch实现&#xff0c;而且还扩展了对多种硬件的适配支持&#xff0c;让模型的部署和…