【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.NotDirty
    if (!this.active) {
      return this.fn()
    }
    let lastShouldTrack = shouldTrack
    let lastEffect = activeEffect
    try {
      //shouldTrack设置为true,允许依赖收集
      shouldTrack = true
      /**
       * this引用的是ReactiveEffect类的当前实例
       * 将全局的activeEffect变量设置为当前的副作用实例
       */
      activeEffect = this
      /**
       * _runnings属性记录了当前副作用函数被执行的次数
       * 这个计数器主要用于内部管理,确保副作用的正确执行和清理
       */
      this._runnings++
      preCleanupEffect(this)
      return this.fn()
    } finally {
      postCleanupEffect(this)
      this._runnings--
      activeEffect = lastEffect
      shouldTrack = 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实例,封装副作用函数fn
  const _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 ReactiveEffectRunner
  runner.effect = _effect
  return 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更新都会被自动进行。

在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

[幻灯片]软件需求设计方法学全程实例剖析-03-业务用例图和业务序列图

DDD领域驱动设计批评文集 做强化自测题获得“软件方法建模师”称号 《软件方法》各章合集 pdf已上传至本号的CSDN资源&#xff0c;或到以下地址下载&#xff1a; http://umlchina.com/training/umlchina_03_bm.pdf

蓝队面经(一)

蓝队面经(一) 文章目录 蓝队面经(一)入侵排查思路windows入侵排查思路Linux入侵排查思路 Linux 如何查看登录日志Windows 和 Linux 的日志文件放在哪里&#xff1f;WindowsLinux Linux 常用排查命令有哪些&#xff1f;Linux 的 Selinux 是什么&#xff1f;如何设置 Selinux&…

【linux课设】自主实现shell命令行解释器

shell和bash的关系 shell是命令解释器&#xff0c;它接收用户的命令并将其传递给内核去执行。bash,即GNU Bourne-Again Shell&#xff0c;是shell的一种实现方式&#xff0c;也是大多数linux系统下默认的shell。 bash的原理 大多数的指令进程&#xff08;除了内建命令&#…

【如何解决一些常见的 Composer 错误的保姆级讲解】

&#x1f308;个人主页:程序员不想敲代码啊&#x1f308; &#x1f3c6;CSDN优质创作者&#xff0c;CSDN实力新星&#xff0c;CSDN博客专家&#x1f3c6; &#x1f44d;点赞⭐评论⭐收藏 &#x1f91d;希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提…

【企业动态】成都九号诶艾科技有限公司基本信息

成都九号诶艾科技有限公司基本情况概述 公司基本信息 成都九号诶艾科技有限公司成立于2023年7月4日,位于中国(四川)自由贸易试验区成都高新区。该公司主要从事人工智能应用软件开发、人工智能基础软件开发以及互联网数据服务等相关业务。此外,九号诶艾科技还涉足旅游开发项…

如何撰写研究论文

SEVENTYFOUR/SHUTTERSTOCK 即使对于有经验的作家来说&#xff0c;将数月或数年的研究浓缩到几页纸中也是一项艰巨的任务。作者需要在令人信服地解决他们的科学问题和详细地呈现他们的结果之间找到最佳平衡点&#xff0c;以至于丢失了关键信息。他们必须简明扼要地描述他们的方…

Android开发 OCR:通过Tesseract实现图片文字识别

下面是整个详解步骤过程 效果图一、OCR的含义二、什么是Tesseract三、前提准备1、添加依赖2、数据文件下载路径 四、实际代码案例Demo如下&#xff1a;Main.xmlMain.java 效果图 流程&#xff1a;获取assets中的图片显示到页面&#xff0c;提取照片内的文字 一、OCR的含义 o…

YoloV8实战:使用YoloV8检测钢材表面缺陷

摘要 本文讲解一下实战,使用YoloV8检测钢材表面缺陷。起因是有粉丝搞不定这个数据集,检测的时候出现了不收敛的情况。所以,给大家讲讲如何去做。 不收敛的原因 一般遇到不收敛的情况,就是数据转化的时候出错了,大家可以打开runs下面的训练日志,观察一下是否正常。 如…

揭秘谷歌Gemini Pro 1.5:如何免费体验处理超长对话的AI模型?

最近Google发布大模型API&#xff0c;让人有点想哭的那种。 他们发布了Gemini Pro&#xff0c;这个东西的免费key每分钟能调用60次&#xff01; 想想看&#xff0c;这速度&#xff0c;比GPT-3.5以前的免费key快了30倍不止。 而且&#xff0c;更厉害的是&#xff0c;即使是Ge…

C语言中的文件和文件操作

目录 为什么会有文件&#xff1f; 文件名 ⼆进制⽂件和⽂本⽂件&#xff1f; ⽂件的打开和关闭 流 标准流 ⽂件指针 ⽂件的打开和关闭 顺序读写函数介绍 对⽐⼀组函数&#xff1a; 文件的随机读写 fseek ftell rewind ⽂件读取结束的判定 被错误使⽤的 feof ⽂件…

TouchGFX之文本区域

文本区域在屏幕上显示文本。 文本区域的文本在大小、颜色、自定义字体、动态文本等方面是完全可以配置的 #ifndef TOUCHGFX_TEXTAREA_HPP #define TOUCHGFX_TEXTAREA_HPP #include <touchgfx/Font.hpp> #include <touchgfx/TextProvider.hpp> #include <touchg…

软件设计不是CRUD(16):低耦合模块设计理论——行为抽象与设计模式(下)

(接上文《软件设计不是CRUD(15):低耦合模块设计理论——行为抽象与设计模式(中)》) 3.2.4、之前的业务逻辑需要关注后续逻辑的执行成败,并调整自身执行的情况 这个场景在之前场景的基础上增加了新的控制要求,具体来说就是之前已经完成的控制逻辑执行,需要在后续控制…

Vue基础配置、组件通信

基础配置 Vue框架已经集成了webpack配置 小注意点 vbase 快速生成vue模板 组件名必须是多词格式(驼峰模式) 具体三种写法: ①小驼峰:abcDef.vue ②大驼峰&#xff1a;AbcDef.vue ③中横线&#xff1a;abc-def.vue 假如文件名不符合多次格式的补救办法&#xff1a; 导出重命名…

MySQL数据库MHA高可用集群

前言 MySQL 数据库 MHA&#xff08;Master High Availability&#xff09;高可用集群是一种用于提高 MySQL 数据库可用性的解决方案。它通过自动故障切换和监控来确保数据库系统在主服务器发生故障时能够快速切换到备用服务器&#xff1b;在 MHA 高可用集群中&#xff0c;Mast…

线程与同步

目录 一&#xff0c;线程概念 1&#xff0c;什么是线程 2&#xff0c;Linux 下的线程 3&#xff0c;POSIX 线程库 二&#xff0c;线程同步 1&#xff0c;数据不一致问题 2&#xff0c;如何解决这类问题 3&#xff0c;死锁 三&#xff0c;线程同步的运用 1&#xff0c…

第十四届蓝桥杯省赛C++ B组所有题目以及题解(C++)【编程题均通过100%测试数据】

第一题《日期统计》【枚举】 【问题描述】 小蓝现在有一个长度为100的数组&#xff0c;数组中的每个元素的值都在0到9的范围之内。数组中的元素从左至右如下所示&#xff1a; 5 6 8 6 9 1 6 1 2 4 9 1 9 8 2 3 6 4 7 7 5 9 5 0 3 8 7 5 8 1 5 8 6 1 8 3 0 3 7 9 2 7 0 5 8 8 …

【Go】五、流程控制

文章目录 1、if2、switch3、for4、for range5、break6、continue7、goto8、return 1、if 条件表达式左右的()是建议省略的if后面一定要有空格&#xff0c;和条件表达式分隔开来{ }一定不能省略if后面可以并列的加入变量的定义 if count : 20;count < 30 {fmt.Println(&quo…

剑指Offer题目笔记22(快速排序)

快速排序定义&#xff1a; ​ 快速排序的基本思想是分治法&#xff0c;排序过程如下&#xff1a;在输入数组中随机选取一个元素作为中间值&#xff08;pivot&#xff09;&#xff0c;然后对数组进行分区&#xff08;partition&#xff09;&#xff0c;使所有比中间值小的数据移…

DC电源模块的分类及特点介绍

BOSHIDA DC电源模块的分类及特点介绍 DC电源模块是一种将交流电转换为直流电的设备&#xff0c;广泛应用于各种电子设备中。根据其特点和功能&#xff0c;DC电源模块可以分为线性稳压模块和开关稳压模块两种。本文将详细介绍这两种DC电源模块的分类和特点&#xff0c;以便读者…

[C++初阶] 爱上C++ : 与C++的第一次约会

&#x1f525;个人主页&#xff1a;guoguoqiang &#x1f525;专栏&#xff1a;我与C的爱恋 本篇内容带大家浅浅的了解一下C中的命名空间。 在c中&#xff0c;名称&#xff08;name&#xff09;可以是符号常量、变量、函数、结构、枚举、类和对象等等。工程越大&#xff0c;名称…