likes
comments
collection
share

「Vue3学习篇」-computed()

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

『引言』

看到标题,想必对computed并不会感到陌生。

在vue2中,需要进行数值计算,并且依赖于其它数据时,使用computed

因为可以利用computed缓存特性,避免每次获取值时,都要重新计算。 在对大数据进行操作时,可以提高性能减少计算次数

🤔🤔那vue3中的computed是怎么使用的❓会有什么不一样❓一起探索一下🤓🤓。

『回顾』

通过一个简单示例回忆一下,vue2中computed的使用。

『写法一』

 <template>
    <div>Hello,大家好,我的全名叫::{{ fullName }}</div>
</template>

<script>
export default {
  data() {
    return {
      firstName: 'wn',
      lastName: 'xx'
    }
  },
  
  computed: {
    fullName() {
      return this.firstName + this.lastName
    }
  }
}
</script>

『写法二』

<template>
  <div>
    <div>Hello,大家好,我的全名叫:{{ fullName }}</div>
    <button @click="changeName">点击按钮修改全名</button>
  </div>
   
</template>

<script>
export default {
  data() {
    return {
      firstName: 'wn',
      lastName: 'xx'
    }
  },
  
  computed: {
    fullName: {
        get() {
          return this.firstName + this.lastName
        },
        set(newValue) {
            this.firstName = `${newValue.firstName}`
            this.lastName = `${newValue.lastName}`
        }
    } 
  },
  methods: {
    changeName() {
        this.fullName = {
            firstName: 'pu',
            lastName: 'pu'
        }
    }
  }
}
</script>

接下来进入正题,看一看今日的【主角】vue3中的computed

『computed()』-0

『定义』

【官方解释】 接受一个 getter 函数,返回一个只读的响应式 ref 对象。该 ref 通过 .value 暴露 getter 函数的返回值。它也可以接受一个带有 get 和 set 函数的对象来创建一个可写的 ref 对象。

『用法』

computed 本身有两种使用方式:

const xxx = computed(() => xxx)

const xxx1 = computed({get: (newValue) => {}, set: (newValue) => {}})

『官网示例🌰』

创建一个只读的计算属性 ref:

const count = ref(1)
const plusOne = computed(() => count.value + 1)

console.log(plusOne.value) // 2

plusOne.value++ // 错误

创建一个可写的计算属性 ref:

const count = ref(1)
const plusOne = computed({
  get: () => count.value + 1,
  set: (val) => {
    count.value = val - 1
  }
})

plusOne.value = 1
console.log(count.value) // 0

『注意⚠️』

  • vue.3.0 中用于从vue 按需导入 computed 计算属性。
  • 如果传入的是一个getter 函数,会返回一个不允许修改的计算属性。
  • 如果传入一个对象,包含get 和 set 函数, 就可以创建一个可以修改的计算属性。

『示例🌰』

<template>
  <div>
    <h2>init count:
      {{ state.count }}
    </h2>
    <h2>init number:
      {{ state.number }}
    </h2>
    <h2>computedCount:
      {{ computedCount }}
    </h2>
    <h2>computedNumber:
      {{ computedNumber }}
    </h2>
    <button @click="changeCount">
      changeCount
    </button>
  </div>  
</template>

<script setup>
import {reactive, computed } from 'vue'

    // reactive
    const state = reactive({
        count: 1,
        number: 10
    })
    // computed getter
    const computedCount = computed(() => {
        return state.count + 10
    })
    // computed set get
    const computedNumber = computed({
        get: () => {
            return state.number + 100
        },
        set: (value) => {
            state.number = value - 50
        }
    })

    const changeCount = () =>{
        state.count++;
        computedNumber.value = 100
    }
</script>

『效果展示』

「Vue3学习篇」-computed()

『computed源码』

computed源码如下⬇️:

这个是computed源码的入口函数。

export function computed<T>(
  getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>,
  debugOptions?: DebuggerOptions,
  isSSR = false
) {
  let getter: ComputedGetter<T>
  let setter: ComputedSetter<T>

  const onlyGetter = isFunction(getterOrOptions)
  if (onlyGetter) {
    getter = getterOrOptions
    setter = __DEV__
      ? () => {
          console.warn('Write operation failed: computed value is readonly')
        }
      : NOOP
  } else {
    getter = getterOrOptions.get
    setter = getterOrOptions.set
  }

  const cRef = new ComputedRefImpl(getter, setter, onlyGetter || !setter, isSSR)

  if (__DEV__ && debugOptions && !isSSR) {
    cRef.effect.onTrack = debugOptions.onTrack
    cRef.effect.onTrigger = debugOptions.onTrigger
  }

  return cRef as any
}

『源码分析』

getterOrOptionscomputed接收函数方法,computed的参数,可以是【getter函数】也可以是【包含get、set的对象】

之后会判断getterOrOptions是否是function,的话,说明computed是可读的不是的话,说明computed是可读可写的

最后就是确定好之后,创建一个ComputedRefImpl实例,并将其返回。ComputedRefImplcomputed源码中的核心。

接着一起看一下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 = false

  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)
      }
    })
    this.effect.computed = this
    this.effect.active = this._cacheable = !isSSR
    this[ReactiveFlags.IS_READONLY] = isReadonly
  }

  get value() {}

  set value(newValue: T) {}
}

『源码分析』

ComputedRefImpl包含以下属性

  • _value: 用来缓存我们计算的结果。
  • effect: 在构造器中创建的ReactiveEffect实例。
  • __v_isRef: 标记为一个ref类型。
  • ReactiveFlags.IS_READONLY: 只读标识。
  • _dirty: 用来控制是否需要重新计算。
  • _cacheable: 是否可缓存,取决于SSR。

还包括constructor和get、set等函数。

先探索一下ComputedRefImpl的构造器。ComputedRefImpl构造器接收四个参数:

  • getter
  • setter
  • isReadonly(是否只读)
  • isSSR(是否为SSR

constructor初始化的过程中,调用了ReactiveEffect,第一次调用ReactiveEffect时,只会传入一个参数getter,第二次之后会就接受两个参数。

『ReactiveEffect源码』

ReactiveEffect源码如下⬇️:

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
    }
  }
}

『ReactiveEffect源码分析』

ReactiveEffect中调用recordEffectScope(this, scope)方法,记录当前 ReactiveEffect 对象的作用域。

recordEffectScope会向 effects 中添加 effect,如果没有传入scope参数,那么在执行recordEffectScope时就会有一个默认的参数为activeEffectScope

 export function recordEffectScope(
    effect: ReactiveEffect
    // scope默认值为activeEffectScope
    scope: EffectScope | undefined = activeEffectScope
) {
if (scope && scope.active) {
      scope.effects.push(effect)
     }
}

ReactiveEffect中的run方法就是执行副作用函数,并且在执行副作用函数的过程中,会收集依赖。

run方法中会先判断当前 ReactiveEffect 对象是否处于活跃状态,如果当前 ReactiveEffect 对象不处于活动状态,直接返回 fn 的执行结果。然后,会找当前 ReactiveEffect 对象的最顶层的父级作用域,记录父级作用域为当前活动的 ReactiveEffect 对象。

run 方法中会判断 deferStop 的值,如果为 true,就会执行 stop 方法ReactiveEffect中的stop方法就是停止当前的ReactiveEffect对象,停止之后,就不会再收集依赖了。

接着创建effect对象,在这里会判断_dirty_dirtyfalse,会将_dirty设置为true。之后会触发更改,调用triggerRefValue函数。

并且此时的this.effect.computed指向this,this.effect.activethis._cacheable在SSR中为false,把isReadonly赋值给ReactiveFlags.IS_READONLY属性。

『triggerRefValue源码』

triggerRefValue源码如下⬇️:

export function triggerRefValue(ref: RefBase<any>, newVal?: any) {
  ref = toRaw(ref)
  const dep = ref.dep
  if (dep) {
    if (__DEV__) {
      triggerEffects(dep, {
        target: ref,
        type: TriggerOpTypes.SET,
        key: 'value',
        newValue: newVal
      })
    } else {
      triggerEffects(dep)
    }
  }
}

『triggerRefValue源码分析』

triggerRefValue会接收两个参数【ref】【newVal】,先会通过toRaw获取ref的原始对象,然后对ref的原始对象中是否有dep属性进行判断,如果有,就触发dep中的依赖。会调用 triggerEffects方法。

『triggerEffects源码』

triggerEffects源码如下⬇️:

export function triggerEffects(
  dep: Dep | ReactiveEffect[],
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  // spread into array for stabilization
  const effects = isArray(dep) ? dep : [...dep]
  for (const effect of effects) {
    if (effect.computed) {
      triggerEffect(effect, debuggerEventExtraInfo)
    }
  }
  for (const effect of effects) {
    if (!effect.computed) {
      triggerEffect(effect, debuggerEventExtraInfo)
    }
  }
}

function triggerEffect(
  effect: ReactiveEffect,
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  if (effect !== activeEffect || effect.allowRecurse) {
    if (__DEV__ && effect.onTrigger) {
      effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))
    }
    if (effect.scheduler) {
      effect.scheduler()
    } else {
      effect.run()
    }
  }
}

『triggerEffect源码分析』

首先是获取effects,dep是一个依赖收集容器,dep实例是一个set结构 [...dep] = [ effect ],真正的依赖是effect实例。

然后会循环依赖列表,如果dep中的effect实例是计算属性的effect,那么就会触发计算属性的effect,执行effect。如果dep中的effect实例不是计算属性的effect,触发非计算属性的effect,执行effect

最后将 dep 解构并生成 effects 数组,会判断是否具有这『activeEffect』『effect.allowRecurse』两个方法, 如果 effect.onTrigger 存在,就会执行,只有开发模式下才会执行,然后判断如果 effect 是一个调度器,就会调用scheduler() ,其它情况调用run()方法。

『ComputedRefImpl中get、set源码』

 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)
  }

『ComputedRefImpl中get、set源码分析』

当读取ComputedRefImpl实例的value属性时,会先使用toRaw获取其原始对象。然后调用trackRefValue进行依赖的收集。

export function trackRefValue(ref: RefBase<any>) {
  // 如果允许收集并且存在activeEffect进行依赖收集
  if (shouldTrack && activeEffect) {
    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()))
    }
  }
}

通过_dirty_cacheable属性判断决定是否需要修改self._value,如果是_dirty或不可以被缓存,那么会将_dirty设置为false,并调用self.effect.run(),修改self._value,最后返回self._value

当修改ComputedRefImpl实例的value属性时,会调用实例的_setter函数。

『vue2与vue3中computed的异同』

『相同点』

  • computed都有缓存的功能。
  • 都可以基于响应式数据进行计算,当响应式数据发生变化时,计算结果也会自动更新。

『不同点』

  • 计算属性的定义方式不同

    1)vue2中,使用getters来定义计算属性。

    2)Vue3中,在setup函数中进行定义。

  • 计算逻辑可以直接访问propsdata

    1)vue2中,getters可以访问propsdata,但是需要使用函数参数传入

    2)Vue3中,计算逻辑可以直接访问propsdata,不需要使用函数参数。

  • 计算逻辑不支持watch,但是可以使用watchEffect

    1)在Vue2中,可以使用watch来监测计算属性的变化。

    2)在Vue3中,计算逻辑不再支持watch,但是可以使用watchEffect来监测变化。

  • 计算逻辑可以定义为响应式的数据

    1)在Vue3中,可以将计算逻辑声明为响应式的数据,这样可以在setup函数中对其进行访问和更改。