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

effect.ts:详解

  • [1. 理解activeEffect](#1. 理解activeEffect)
    • [1.1 定义](#1.1 定义)
    • [1.2 通过一个例子来说明这个过程](#1.2 通过一个例子来说明这个过程)
      • [a. 副作用函数的初始化](#a. 副作用函数的初始化)
      • [b. 执行副作用函数前](#b. 执行副作用函数前)
      • [c. 访问state.count](#c. 访问state.count)
      • [d. get拦截器中的track调用](#d. get拦截器中的track调用)
      • [e. 修改state.count时的set拦截器](#e. 修改state.count时的set拦截器)
      • [f. trigger函数中的依赖重新执行](#f. trigger函数中的依赖重新执行)
    • [1.3 实战应用](#1.3 实战应用)
    • [1.4 activeEffect的作用:](#1.4 activeEffect的作用:)
  • [2. 理解ReactiveEffect](#2. 理解ReactiveEffect)
    • [2.1 定义](#2.1 定义)
    • [2.2 源码解析](#2.2 源码解析)
    • [2.3 重要属性解释](#2.3 重要属性解释)
  • [3. 理解effect函数](#3. 理解effect函数)
    • [3.1 定义](#3.1 定义)
    • [3.2 源码解析](#3.2 源码解析)
    • [3.3 工作原理](#3.3 工作原理)
      • [3.3.1 封装副作用函数](#3.3.1 封装副作用函数)
      • [3.3.2 自动执行和响应](#3.3.2 自动执行和响应)
      • [3.3.3 控制执行](#3.3.3 控制执行)
    • [3.4 runner解析](#3.4 runner解析)
  • [4. 小结](#4. 小结)
    • [4.1 副作用函数的初始化](#4.1 副作用函数的初始化)
    • [4.2 依赖收集(Tracking)](#4.2 依赖收集(Tracking))
    • [4.3 触发更新(Triggering)](#4.3 触发更新(Triggering))
    • [4.4 重新执行副作用函数](#4.4 重新执行副作用函数)

在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 通过一个例子来说明这个过程

js 复制代码
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 源码解析

js 复制代码
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 源码解析

js 复制代码
/**
 * 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解析

js 复制代码
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更新都会被自动进行。

相关推荐
胡西风_foxww15 分钟前
【ES6复习笔记】数值扩展(16)
前端·笔记·es6·扩展·数值
mosen86817 分钟前
uniapp中uni.scss如何引入页面内或生效
前端·uni-app·scss
白云~️17 分钟前
uniappX 移动端单行/多行文字隐藏显示省略号
开发语言·前端·javascript
沙尘暴炒饭19 分钟前
uniapp 前端解决精度丢失的问题 (后端返回分布式id)
前端·uni-app
昙鱼33 分钟前
springboot创建web项目
java·前端·spring boot·后端·spring·maven
天天进步201539 分钟前
Vue项目重构实践:如何构建可维护的企业级应用
前端·vue.js·重构
2402_8575834939 分钟前
“协同过滤技术实战”:网上书城系统的设计与实现
java·开发语言·vue.js·科技·mfc
小华同学ai42 分钟前
vue-office:Star 4.2k,款支持多种Office文件预览的Vue组件库,一站式Office文件预览方案,真心不错
前端·javascript·vue.js·开源·github·office
北京_宏哥43 分钟前
python接口自动化(四十二)- 项目架构设计之大结局(超详解)
python·架构·前端框架
APP 肖提莫43 分钟前
MyBatis-Plus分页拦截器,源码的重构(重构total总数的计算逻辑)
java·前端·算法