当前位置: 首页 > article >正文

vue3源码分析 -- computed

computed计算属性是存在依赖关系,当依赖的值发生变化时计算属性也随之变化

案例

首先引入reactiveeffectcomputed三个函数,声明obj响应式数据和computedObj计算属性,接着执行effect函数,该函数传入了一个匿名函数进行computedObj的赋值,最后两秒后修改objname

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="../../dist/vue.global.js"></script>
  </head>
  <body>
    <div id="app"></div>
    <script>
      const { reactive, effect, computed } = Vue
      // 创建响应式数据
      const obj = reactive({
        name: 'jc'
      })
      // 计算属性 触发 obj.name 的 get 行为
      const computedObj = computed(() => {
        return '姓名:' + obj.name
      })
      // effect 函数中 触发 计算属性的 get 行为
      effect(() => {
        document.querySelector('#app').innerHTML = computedObj.value
      })
      
      // 修改响应式数据的 name 值 触发 set 行为
      setTimeout(() => {
        obj.name = 'cc'
      }, 2000)
    </script>
  </body>
</html>

ComputedRefImpl 实例

computed函数定义在packages/reactivity/src/computed.ts文件下:

在这里插入图片描述

该函数接收 getterOrOptions 参数,即我们传入的匿名函数() => { return '姓名:' + obj.name }

在这里插入图片描述

之后赋值给gettersetter我们可以理解为一个空函数,之后创建一个 ComputedRefImpl 实例,并将其返回,看下ComputedRefImpl构造函数:

export class ComputedRefImpl<T> {
  public dep?: Dep = undefined

  private _value!: T
  public readonly effect: ReactiveEffect<T>

  public readonly __v_isRef = true
  public readonly [ReactiveFlags.IS_READONLY]: boolean

  public _dirty = true // 脏变量 关键
  public _cacheable: boolean

  constructor(
    getter: ComputedGetter<T>,
    private readonly _setter: ComputedSetter<T>,
    isReadonly: boolean,
    isSSR: boolean
  ) {
    this.effect = new ReactiveEffect(getter, () => {
      if (!this._dirty) {
        this._dirty = true
        triggerRefValue(this) // dirty 为 false 时 触发依赖
      }
    })
    this.effect.computed = this
    this.effect.active = this._cacheable = !isSSR
    this[ReactiveFlags.IS_READONLY] = isReadonly
  }

  get value() {
    // the computed ref may get wrapped by other proxies e.g. readonly() #3376
    const self = toRaw(this)
    trackRefValue(self) // 依赖收集
    if (self._dirty || !self._cacheable) {
      self._dirty = false
      self._value = self.effect.run()!
    }
    return self._value
  }

  set value(newValue: T) {
    this._setter(newValue)
  }
}

该构造函数会创建一个ReactiveEffect实例,这块逻辑就不再赘述,先看下返回的实例effect

在这里插入图片描述

另外还需关心 ReactiveEffect 传入的第二个参数 scheduler,该构造函数在packages/reactivity/src/effect.ts文件下:

scheduler我们可以理解为一个调度器,这也是**computed** 核心所在,该逻辑会进行依赖触发

// 传入的参数
() => {
    if (!this._dirty) {
        this._dirty = true
        triggerRefValue(this) // dirty 为 false 时 触发依赖
    }
}

// ReactiveEffect 构造函数
export class ReactiveEffect<T = any> {
  // 省略
  constructor(
    public fn: () => T,
    public scheduler: EffectScheduler | null = null,
    scope?: EffectScope
  ) {
    recordEffectScope(this, scope)
  }
  // 省略
}  

ComputedRefImpl定义了一个_dirty脏变量,还定义了get valueset value两个方法,这也是和ref相同,赋值时需带上.value属性的原因

get value 会进行依赖收集,但是依赖触发并没有在 set value 中,而是在我们之前 ReactiveEffect 传入的第二个参数中

此时computed函数执行完毕返回ComputedRefImpl实例对象:

在这里插入图片描述

之后执行effect函数,进行赋值document.querySelector('#app').innerHTML = computedObj.value,从而触发computedget value方法:

在这里插入图片描述

get/set

trackRefValue(self)进行依赖收集,该方法之前也讲到过。由于此时_dirty脏变量为true(默认为true),所以之后设置为 false ,再执行 self.effect.run() 进行赋值

effect.run()实际执行的是fn()方法,即computed传入的匿名函数() => { return '姓名:' + obj.name }effect函数执行完毕,页面呈现如下:

在这里插入图片描述

两秒后触发objsetter行为,即执行createSetter方法进行trigger依赖触发(第一次),然后根据name属性获取到对应的effects,该逻辑都在packages/reactivity/src/effect.ts文件下:

在这里插入图片描述

之后triggerEffects函数遍历effects,执行triggerEffect(effect, debuggerEventExtraInfo)

在这里插入图片描述

这里需要关注下if (effect.scheduler)判断逻辑,由于此时执行的**effect** 含有 computed 属性,且存在 scheduler,则会执行effect.scheduler()方法:

在这里插入图片描述

这就是之前提到的ComputedRefImpl构造函数中,创建ReactiveEffect实例时传入的第二个参数:

export class ComputedRefImpl<T> {
  // 省略
  constructor(
    getter: ComputedGetter<T>,
    private readonly _setter: ComputedSetter<T>,
    isReadonly: boolean,
    isSSR: boolean
  ) {
    this.effect = new ReactiveEffect(getter, () => {
      if (!this._dirty) {
        this._dirty = true
        triggerRefValue(this) // dirty 为 false 时 触发依赖
      }
    })
   // 省略
  }

  // 省略
}

// 第二个参数
() => {
    if (!this._dirty) {
        this._dirty = true
        triggerRefValue(this) // dirty 为 false 时 触发依赖
    }
}

因为在computed函数的get value方法中,_dirty设置了false,所以直接走判断逻辑,将 _dirty 设置为 false ,执行 triggerRefValue(this) 依赖触发(第二次) ,所以computed的依赖触发是在该逻辑中执行的,这里是关键

此时获取到的effects

在这里插入图片描述

所以根据判断逻辑直接 effect.run() ,执行run等于执行fn方法,即执行effect传入的匿名函数,之后执行document.querySelector('#app').innerHTML = computedObj.value赋值操作,再次触发computedget value方法:

get value() {
    // the computed ref may get wrapped by other proxies e.g. readonly() #3376
    const self = toRaw(this)
    trackRefValue(self) // 依赖收集
    if (self._dirty || !self._cacheable) {
      self._dirty = false
      self._value = self.effect.run()!
    }
    return self._value
}

接着执行self._value = self.effect.run()!,又再次执行computed传入的匿名函数() => { return '姓名:' + obj.name }重新赋值:

在这里插入图片描述

代码执行完成,此时页面呈现修改后的值:

在这里插入图片描述

总结

1. computed 计算属性实际是一个 ComputedRefImpl 构造函数的实例

ComputedRefImpl构造函数中通过_dirty变量来控制effectrun方法的执行和triggerRefValue的触发

  • 初始时_dirtytrue,表示计算属性的值尚未计算,需要重新计算
  • 当值被计算并缓存后,_dirty设置为false
  • 当计算属性的依赖发生变化时,例如响应式数据被修改,_dirty重新设置为true,表示需要重新计算值

2.想要访问计算属性的值,必须通过 .value ,因为它内部和 ref 一样是通过 get value 来进行实现的

每次.value时都会执行get value方法,从而触发trackRefValue进行依赖收集

3. 在依赖触发时,先触发**computedeffect** ,再触发非**computedeffect**

1)避免依赖循环和死锁

当计算属性和普通 effect 之间存在双向依赖时,若同时触发两者的更新,可能导致无限循环。例如:计算属性 A 的更新触发了普通 effect B,B 的修改又触发了 A 的依赖更新

先计算属性,后普通 effect 可以打破这种循环,计算属性的更新完成后,普通 effect 再基于最新的计算值执行,从而避免循环

2)确保计算属性的值是最新状态

计算属性的核心是缓存机制,其值需要基于依赖项的最新状态计算。若普通 effect 先执行,可能导致计算属性在后续访问时仍使用旧的缓存值(即脏状态未更新),从而产生错误结果

3)性能优化

计算属性的更新通常涉及复杂计算,而普通 effect 可能包含 DOM 操作等耗时任务。通过分阶段触发:

  • 减少重复计算:若普通 effect 先触发并修改了响应式数据,可能导致计算属性多次重新计算
  • 合并更新:计算属性的更新完成后,普通 effect 可以基于最终状态一次性完成渲染,减少重复渲染

http://www.kler.cn/a/595483.html

相关文章:

  • 深度解析学术论文成果评估(Artifact Evaluation):从历史到现状
  • 【问题解决】Postman 测试报错 406
  • 深入理解Java虚拟机(学习笔记)
  • java基础--序列化与反序列化的概念是什么?
  • 关于FastAPI框架的面试题及答案解析
  • 查看visual studio的MSVC版本的方法
  • 23 种设计模式中的访问者模式
  • 零基础上手Python数据分析 (7):Python 面向对象编程初步
  • 蓝桥杯 之 暴力回溯
  • 3.16[A]FPGA
  • Pytest基础使用
  • Netty源码—3.Reactor线程模型三
  • L2TP实验报告
  • 无服务器架构将淘汰运维?2025年云计算形态预测
  • RabbitMQ 与 Kafka:消息中间件的终极对比与选型指南
  • MSE分类时梯度消失的问题详解和交叉熵损失的梯度推导
  • Redis哨兵模式(Sentinel)高可用方案介绍与配置实践
  • 数字孪生技术引领UI前端设计新风尚:跨平台与响应式设计的结合
  • 【Bluebell】项目总结:基于 golang 的前后端分离 web 项目实战
  • ue5蓝图项目转换为c++项目 遇到的问题