vue3学习与my-vue3实现07:computed

98 阅读4分钟

结合前面实现的功能,在本章,我们可以实现vue.js中一个非常重要且特色的能力—计算属性。

computed

计算属性的实现,如下所示:

//reactivity/src/computed.ts
import { effect } from './effect'

export type ComputedGetter<T> = (...args: any[]) => T

export interface ComputedRef<T = any> {
  readonly value: T
}
export class ComputedRefImpl<T> {
  private readonly effect: ReactiveEffect
  constructor(
    getter: ComputedGetter<T>,
  ) {
    // 把getter作为副作用函数,创建一个ReactiveEffect
    this.effect = new ReactiveEffect(getter)
  }

  get value() {
    // 当读取value的时候才执行effect
    return this.effect.run()
  }
}
export function computed<T>(
  getter: ComputedGetter<T>,
): ComputedRef<T> {
  const cRef = new ComputedRefImpl(getter)

  return cRef as any
}

参照vue3源码,我们定义了一个computed函数,它接受getter函数作为参数,在函数内部实例化一个ComputedRefImpl对象,ComputedRefImpl在构造函数中创建了一个ReactiveEffect实例,computed函数返回ComputedRefImpl对象,而该对象value属性是一个访问属性,只有当读取value的值时,才会执行副作用函数并将其结果作为返回值。

将computed函数导出:

// reactivity/src/index.ts
export { computed } from './computed'
// vue/src/index.ts
export { reactive, effect, scheduler, computed } from '@my-vue3/reactivity'

我们可以使用computed函数来创建一个计算属性:

const { reactive, computed } = Vue
const obj = reactive({
  foo: 1,
})

const res = computed(() => obj.foo + 1)
console.log(res.value)

值缓存

上面实现的计算属性只能做到懒计算,但还做不到对值进行缓存,假使我们多次访问res.value的值,会导致副作用函数多次计算,即使obj.foo的值本身并没有变。

为了解决这个问题,就需要我们在实现ComputedRefImpl时,添加对值进行缓存的功能,如以下代码所示:

//reactivity/src/computed.ts
export class ComputedRefImpl<T> {
  private readonly effect: ReactiveEffect
  // value缓存上一次计算的值
  private _value!: T

  // dirty标志,用来标识是否需要重新计算值,为true则意味着"脏",需要计算
  public _dirty = true

  constructor(
    getter: ComputedGetter<T>,
  ) {
		this.effect = new ReactiveEffect(getter)
  }

  get value() {
    if (this._dirty) {
      this._dirty = false
      this._value = this.effect()!
    }
    return this._value
  }
}

这里新增了两个变量valuedirty,其中value用来缓存上一次计算的值,而dirty是个标识,代表是否需要重新计算。当我们通过res.value访问时,只有当dirtytrue时才会调用effect重新计算,否则直接使用上一次缓存在value的值。这样无论我们访问多少次res.value,都只会在第一次访问时进行真正的计算,后续访问都会直接读取缓存的value值。

重新计算

接着这里也带来了新的问题,如果我们修改了obj.foo的值,在访问res.value会发现访问到的值没有发生变化:

const obj = reactive({
  foo: 1,
})

const res = computed(() => obj.foo + 1)
console.log(res.value)
obj.foo++
// 值仍然是2
console.log(res.value)

这是由于第一次访问res.value值后,dirty标识会设置为false,代表不需要计算。即使我们修改了obj.foo的值,但只要dirty标识为false,就不会重新计算,所以导致我们得到了错误的值。而解决方法就是当obj.foo的值变化时,dirty的值重置为true就可以了,这里就需要我们用到之前实现的scheduler,代码如下:

//reactivity/src/computed.ts
export class ComputedRefImpl<T> {
  private readonly effect: ReactiveEffect
  private _value!: T
  public _dirty = true

  constructor(
    getter: ComputedGetter<T>,
  ) {
		this.effect = new ReactiveEffect(getter, () => {
			//在调度器中将dirty重置为true
      if (!this._dirty)
        this._dirty = true
    })
  }

  get value() {
    if (this._dirty) {
      this._dirty = false
      this._value = this.effect()!
    }
    return this._value
  }
}

我们为effect添加了scheduler调度器函数,它会在getter函数中所依赖的响应式数据变化时执行,这样我们在scheduler函数内将dirty重置为true,下一次访问res.value时,就会重新调用effect计算值,这样就能够得到预期的结果了。

computed的effect嵌套

此时我们的computed还有一个问题,体现在当我们在一个effect函数中读取计算属性时:

const obj = reactive({
  foo: 1,
})

const res = computed(() => {
  return obj.foo + 1
})
effect(() => {
	// 在该副作用函数中读取res.value
  console.log(res.value)
})
// 修改obj.foo的值,并不会触发effect注册的副作用函数
obj.foo++

如以上代码所示,res是一个计算属性,并且在另一个effect的副作用函数中读取了res.value的值。如果修改了obj.foo的值,我们期望副作用函数重新执行。但是如果尝试运行上面这段代码,会发现修改obj.foo的值并不会触发副作用函数的执行。

尝试分析这个问题的原因我们发现这是一个effect嵌套。一个计算属性内部拥有自己的effect,并且它是懒执行的,只有当真正读取计算属性的值时才会执行。对于计算属性的getter函数来说,它里面访问的响应式数据只会把computed内部的effect收集为依赖。而当把计算属性用于另外一个effect时,就会发生effect嵌套,外层的effect不会被内层effect中的响应式数据收集。

而解决办法就是读取计算属性的值时,我们需要手动调用track函数进行追踪,当计算属性依赖的响应式数据发生变化时,我们可以手动调用trigger函数触发响应。如一下代码所示:

//reactivity/src/computed.ts
export class ComputedRefImpl<T> {
  private readonly effect: ReactiveEffect
  private _value!: T
  public _dirty = true

  constructor(
    getter: ComputedGetter<T>,
  ) {
    this.effect = new ReactiveEffect(getter, () => {
      if (!this._dirty) {
        this._dirty = true
        // 当计算属性依赖的响应式数据发生变化时, 手动调用trigger函数触发响应
        trigger(this, 'value')
      }
    })
  }

  get value() {
    if (this._dirty) {
      this._dirty = false
      this._value = this.effect.run()
    }
    // 当读取value时,手动调用track函数进行追踪
    track(this, 'value')
    return this._value
  }
}

代码仓库

github.com/KoiraCMT/my…