likes
comments
collection
share

Vue3源码之Computed

作者站长头像
站长
· 阅读数 15

前言

主题: 本文将深入探讨Vue3.2.47computed 的实现

内容:本文分为三个部分:首先是介绍及使用,然后是源码逐行阅读,最后总结及注意事项

目的:手模手深入学习computed原理,方便开发排问题及面试派上用场😁


Vue3源码之Reactive

Vue3源码之拦截工具函数

Vue3源码之ref + 依赖收集 + 通知更新


一、computed介绍

1. computed 基本使用

computed 是Vue中的一个计算属性。它的本质是一个函数,可以根据依赖数据计算并返回一个新值,只有当它依赖的数据发生了变化,它才会重新计算。因为在template中对数据进行复杂计算一般使用它代替。

官方介绍:传入一个 getter 函数,返回一个默认不可手动修改的 ref 对象。

const count = ref(1)
const numb = computed(() => count.value *2) // count.value改变它才会重新计算
console.log(numb.value) // 2
count.value = 2
console.log(numb.value) // 4
numb.value ++ // 控制台会警告 Write operation failed: computed value is readonly

或者传入一个拥有 get 和 set 函数的对象,创建一个可手动修改的计算状态。

const count = ref(2)
const num = computed({
  set(val: number) {
    count.value = val
  },
  get() {
    return count.value++
  }
})
console.log(num.value) // 3
num.value = 33
console.log(num.value) // 33
console.log(count.value) // 34

2. computed 使用场景

  • 对列表数据进行过滤、排序、计算,返回新的数组
  • 同时依赖价格、数量,多个商品,返回总价
  • 配合 v-model 使用的比较多
  • 子组件 依赖父组件状态,并需要emit 通知父组件改值时
const emit = defineEmits(['changeShow'])
const props =defineProps({
  show: {
    type: Boolean,
    default: false
  }
})
const isShow = computed({
  set(val){
    emit('changeShow', val)
  },
  get() {
    return props.show
  }
})

相信大家也经常听到别人说,computed 计算属性有缓存、可以优化性能??

3. computed 缓存表现

<template>
  <div>
    <h3>{{ fn() }}</h3>
    <h3>{{ fn() }}</h3>
    <h3>{{ count }}</h3>
    <h3>{{ count }}</h3>
  </div>
</template>

<script lang="ts" setup>
  const fn = () => {
    console.log('fn被执行了...')
    return 'fn被执行了...'
  }
  // 使用的 nuxt3 所以不需要引入
  const count = computed(() => {
    console.log('computed进行了计算..')
    return 'computed进行了计算..'
  })
</script>

上面的代码是把computed 和 方法 做了对吧,我们来看看它们的打印情况

Vue3源码之Computed

我们回顾了computed的使用及缓存的表现,接下来!上主菜👻

二 、computed源码

源码地址

1. computed 主函数

前置知识点

  1. computed 从代码层面来看 computed本质是 一个 ref 对象,使用层面它是一个计算属性
  2. 只传 get函数 返回的是 只读 ref ; 修改值,需要传对象,且需包含get 、set 函数
  3. computed 参数一般两种: 一种get函数 ;另一种对象 {set get} ;注意get函数需要有返回值
// https://github1s.com/vuejs/core/blob/v3.2.47/packages/reactivity/src/computed.ts#L79-L108
export function computed<T>(
  getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>,
  debugOptions?: DebuggerOptions,
  isSSR = false
) {
  let getter: ComputedGetter<T>
  let setter: ComputedSetter<T>

	// 如果传入的getterOrOptions是函数说明只传了 get ==> comput(()=> {})
  // 只读
  const onlyGetter = isFunction(getterOrOptions)
  if (onlyGetter) {
    getter = getterOrOptions
    // 只传get , dev 环境下,修改computed 的值会抛出警告,pro 啥也不做
    setter = __DEV__
      ? () => {
          console.warn('Write operation failed: computed value is readonly')
        }
      : NOOP // export const NOOP = () => {}
  } else {
    // 传入是对象的,初始化赋值 getter\setter
    getter = getterOrOptions.get
    setter = getterOrOptions.set
  }
	// 创建 ComputedRefImpl实例 基于 RefImpl 类进行实现【解释一下前面说的本质是ref】
  const cRef = new ComputedRefImpl(getter, setter, onlyGetter || !setter, isSSR)

	// dev环境 && 传了 debugOptions && 不是服务端渲染
  if (__DEV__ && debugOptions && !isSSR) {
    // 开发者可以 在dev 下 监听 依赖收集、触发更新的回调
    cRef.effect.onTrack = debugOptions.onTrack
    cRef.effect.onTrigger = debugOptions.onTrigger
  }

	// 返回 ComputedRefImpl实例
  return cRef as any
}

从上面的源码中我们可以知道:computed函数主要是做了两件事:

  1. 初始化并赋值 getter setter,没有传setter 则使用 () => {}
  2. 实例化并返回 ComputedRefImpl核心类

下面我们来看看 ComputedRefImpl 核心类中具体做了什么

2. ComputedRefImpl 核心类

ComputedRefImpl 是计算属性的核心类

  • dep 保存计算属性所依赖的响应式对象的依赖关系
  • _value 缓存的值
  • effect 副作用对象
  • __v_isRef ref 标识
  • _dirty 缓存是否过期标识
  • trackRefValue 属性被访问时进行依赖收集
// https://github1s.com/vuejs/core/blob/v3.2.47/packages/reactivity/src/computed.ts#L26-L69
export class ComputedRefImpl<T> {
  public dep?: Dep = undefined


  private _value!: T  // 缓存的值
  public readonly effect: ReactiveEffect<T> // 副作用对象


  public readonly __v_isRef = true // ref 标识
  public readonly [ReactiveFlags.IS_READONLY]: boolean = false // 只读标识


  public _dirty = true // 脏数据【缓存是否过期标识】
  public _cacheable: boolean // 是否能被缓存


  constructor(
    getter: ComputedGetter<T>,
    private readonly _setter: ComputedSetter<T>,
    isReadonly: boolean,
    isSSR: boolean
  ) {
    // 创建了一个 ReactiveEffect 用存储计算属性的值和依赖项
    // effect 用于获取计算属性对应的响应式对象
    // 在计算属性被访问时,如果缓存过期 则self.effect.run()! 重新计算
    this.effect = new ReactiveEffect(getter, () => {
      // getter => fn 
      // getter 后面的回调是 更新函数的执行调度器,用于在执行更新函数前后进行一些处理
      if (!this._dirty) {
        this._dirty = true
        triggerRefValue(this)
      }
    })
    // computed 用于获取计算属性的getter 
    this.effect.computed = this
    
    // _cacheable 默认true, isSSR 表示服务端渲染
    // 表示 需不需要进行缓存
    this.effect.active = this._cacheable = !isSSR
    // 只读
    this[ReactiveFlags.IS_READONLY] = isReadonly
  }

	// get 拦截器
  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) { // _dirty 脏 || 不能缓存
      self._dirty = false // 先把 _dirty 设置为false 缓存过期
      self._value = self.effect.run()! // 响应式对象.run 的返回值赋值给 _value
    }
    return self._value
  }

	// set 拦截
  set value(newValue: T) {
    // 执行 传进来的setter函数
    this._setter(newValue)
  }
}

3. ReactiveEffect

ReactiveEffect 是 effect函数的实现类。effect 函数返回一个 ReactiveEffect 对象,用于追踪副作用函数的执行、触发响应式对象的拦截操作、停止追踪时做一些清理 cleanupEffect

// https://github1s.com/vuejs/core/blob/v3.2.47/packages/reactivity/src/effect.ts#L53-L140
export class ReactiveEffect<T = any> {
  active = true
  deps: Dep[] = []
  parent: ReactiveEffect | undefined = undefined


  /**
   * Can be attached after creation
   * @internal
   */
  computed?: ComputedRefImpl<T>
  /**
   * @internal
   */
  allowRecurse?: boolean
  /**
   * @internal
   */
  private deferStop?: boolean


  onStop?: () => void
  // dev only
  onTrack?: (event: DebuggerEvent) => void
  // dev only
  onTrigger?: (event: DebuggerEvent) => void


  constructor(
    public fn: () => T,
    public scheduler: EffectScheduler | null = null,
    scope?: EffectScope
  ) {
    recordEffectScope(this, scope)
  }


  run() {
    if (!this.active) {
      return this.fn()
    }
    let parent: ReactiveEffect | undefined = activeEffect
    let lastShouldTrack = shouldTrack
    while (parent) {
      if (parent === this) {
        return
      }
      parent = parent.parent
    }
    try {
      this.parent = activeEffect
      activeEffect = this
      shouldTrack = true


      trackOpBit = 1 << ++effectTrackDepth


      if (effectTrackDepth <= maxMarkerBits) {
        initDepMarkers(this)
      } else {
        cleanupEffect(this)
      }
      return this.fn()
    } finally {
      if (effectTrackDepth <= maxMarkerBits) {
        finalizeDepMarkers(this)
      }


      trackOpBit = 1 << --effectTrackDepth


      activeEffect = this.parent
      shouldTrack = lastShouldTrack
      this.parent = undefined


      if (this.deferStop) {
        this.stop()
      }
    }
  }


  stop() {
    // stopped while running itself - defer the cleanup
    if (activeEffect === this) {
      this.deferStop = true
    } else if (this.active) {
      cleanupEffect(this)
      if (this.onStop) {
        this.onStop()
      }
      this.active = false
    }
  }
}

4. trackRefValue

trackRefValue 依赖收集

  1. 判断 shouldTrack 和 activeEffect,存在则往下执行
  2. activeEffect 是一个全局变量,执行get 依赖收集时会赋值为副作用函数, 副作用函数执行之后,activeEffect 会赋值为null 【这里只需先知道有这么个事儿 就好了】
  3. 拿到原始值
  4. 调用trackEffects 并把 ref.dep 传进去,刚开始 ref.dep是 undefined,然后创建dep
// https://github1s.com/vuejs/core/blob/v3.2.47/packages/reactivity/src/ref.ts#L40-L53
export function trackRefValue(ref: RefBase<any>) {
  // shouldTrack为true => 处于响应式状态
  // activeEffect不为undefined => 存在运行的Effect函数
  if (shouldTrack && activeEffect) {
    // 拿到原始值
    // 调用 trackEffects, 把ref.dep 传进去,没有则创建
    ref = toRaw(ref)
    if (__DEV__) { // 开发环境 记录一些调试信息
      trackEffects(ref.dep || (ref.dep = createDep()), {
        target: ref,
        type: TrackOpTypes.GET,
        key: 'value'
      })
    } else {
      trackEffects(ref.dep || (ref.dep = createDep()))
    }
  }
}

5. trackEffects

trackEffects 函数就是根据传过来的dep 对象,将当前的副作用加入到指定的 dep 中,以便依赖跟新时 触发副作用函数跟新。这个模块比较绕,我们先梳理一下几个概念

  1. 每个computed 对象是独立的,所以每个computed 有一个对应的 dep对象来存储 这个computed 的所以依赖项(就是所有用到这个computed 的地方),每个依赖项会存在一个或多个的 副作用函数,副作用函数被存储在依赖项的deps 中(activeEffect.deps.push(dep))
  2. 当一个响应式对象computed\ref\reactive被访问时,它所对应的 dep 对象就会被加入当前正在执行的副作用函数(即 activeEffect)的依赖列表中(activeEffect.deps.push(dep) ),同时这个 dep 对象也会记录下这个副作用函数。
  3. 函数副作用 ****指函数在正常工作任务之外对外部环境所施加的影响
// https://github1s.com/vuejs/core/blob/v3.2.47/packages/reactivity/src/effect.ts#L232-L257
const maxMarkerBits = 30

export function trackEffects(
  dep: Dep, // 依赖集合
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  let shouldTrack = false // 是否应该 追踪依赖关系
  if (effectTrackDepth <= maxMarkerBits /*20*/) { // 效果追踪深度小于等于最大标记位数
    if (!newTracked(dep)) { // 当前的 effect 不存在了 dep
      dep.n |= trackOpBit // 设置新的 追踪标记
      shouldTrack = !wasTracked(dep) // 判断是否应该被追踪
    }
  } else {
    // Full cleanup mode. 完全清理模式
    shouldTrack = !dep.has(activeEffect!)
  }


  /*
  * effect 响应式副作用函数
  * activeEffect 是一个全局变量 表示当前正在运行的 effect  
  * ReactiveEffect 表示响应式对象的订阅者
  * activeEffect!.deps 用于存储当前 effect 依赖的所有 dep 对象
  */
  // 依赖应该被追踪 => 将 dep 和 副作用 双向关联
  if (shouldTrack) {
    // 将当前正在执行的 activeEffect 添加到dep
    // 谁用到了这个响应式数据 就把谁添加到依赖数组中
    dep.add(activeEffect!)
    // 将 dep 添加到 activeEffect 的 deps 数组中,表示 activeEffect 依赖于该 dep。
    activeEffect!.deps.push(dep) 
    if (__DEV__ && activeEffect!.onTrack) {
      activeEffect!.onTrack({
        effect: activeEffect!,
        ...debuggerEventExtraInfo!
      })
    }
  }
}

总结

1. computed 触发计算的场景

  • 计算属性被读取时
  • 计算属性的依赖发生变化时

在第一次访问计算属性时

在js 中 或者 template中,计算属性会进行第一次计算:先进行依赖的收集,初始化 时_dirt为true,通过 _dirty 属性判断缓存的值是否需要重新计算,需要重新计算则先把_dirty属性置为false,然后执行effect.run,将计算结果缓存在_value中,后面访问计算属性时,_dirty是false 直接返回缓存的值,如果_dirt,run 其实就是入参传的的gtter函数,只是里面做了一些处理,最后返回计算结果。

计算属性的依赖发生变化时

当计算属性的依赖发生改时,计算属性并不会立即重新进行计算,【注意:他只是会先把_dirty 设置为false,标记为脏数据,数据已过期】,然后在我们下一次进行访问计算属性时,触发getter 判断_dirty 为脏,才会重新进行计算,并缓存其计算的结果

2. dirty缓存的关键

computed 计算属性可以实现缓存,进行性能的提升,主要靠它。

computed 被访问时,如果 _dirty 的值为 true,说明缓存的计算结果已经过期了,需要重新计算。

依赖项发生改变时,_dirty的值会被置为false,下次访问才会进行计算


下一期进行effect的解读