【mini-vue】实现 computed

55 阅读4分钟

实现computed

主要思路

computed()API 需要传入一个函数,这个函数我们在这里把它称为getter。在前面的reactivity模块中,其实也就是对象属性值所依赖的effect函数。

根据computed属性逐步实现,大致分为3个部分,每个部分都有对应的思路。

过程1: 基本实现

先简单实现一个 computed 函数,方法返回一个ComputedRefImpl 的实例。

class ComputedRefImpl {
  private _getter: any
  constructor(getter) {
    this._getter = getter
  }

  get value() {
    return this._getter()
  }
}
export function computed(getter) {
  return new ComputedRefImpl(getter)
}

实现缓存:当依赖的数据没有发生变化时,getter不会被调用

但是问题是,这里读取了两次 cValue.value, getter被调用了两次

image.png

因此接下来实现缓存的功能

过程2: 缓存

设置一个变量初始值为true(第一次访问数据的时候需要执行getter),当执行完getter操作时将变量置为false,封锁住getter的执行。【实际上,这个变量标志着当前的getter有没有被调用过】

class ComputedRefImpl {
  private _getter: any
  private _dirty: boolean = true
  private _value: any
  constructor(getter) {
    this._getter = getter
  }

  get value() {
    if (this._dirty) {
      this._dirty = false
      this._value = this._getter()
    }
    return this._value
  }
}
export function computed(getter) {
  return new ComputedRefImpl(getter)
}

那什么时候出发getter呢?

过程3: 当依赖的数据发生变化时,[并且! 用到了目标数据的时候], 触发getter操作

既然要实现数据发生变化触发对应的操作,这个过程再熟悉不过了,前面的effect.ts里的trigger函数不就是专门干这件事情的吗? 把它拿过来,再把getter函数传入就行了。

首先创建一个ReactiveEffect 实例,用_effect实例去搜集getter函数

import { trigger } from './effect'
import { ReactiveEffect } from './effect'
class ComputedRefImpl {
  private _getter: any
  private _dirty: boolean = true
  private _value: any
  private _effect: any
  constructor(getter) {
    this._getter = getter
    this._effect = new ReactiveEffect(getter)
  }

  get value() {
    if (this._dirty) {
      this._dirty = false
      this._value = this._effect.run()  
    }
    return this._value
  }
}
export function computed(getter) {
  return new ComputedRefImpl(getter)
}

但还没有达到效果, getter 被执行了两次

image.png

为啥呢???? 这里迷惑了很久, 原因是,value.foo 这里的value本身是个reactive对象,当其发生更改的时候本来就会去触发对应的依赖函数一次,也就是getter。然后再加上前面的cValue.value 初始化的时候访问了一次,所以getter最开始就执行了一次。因此到这里的时候就执行了两次。

怎么把它变成只执行了一次呢? 也就是说,如何做到,只有当我们读取真正需要的值的时候,getter才会执行呢?

利用scheduler。

之前实现ReactiveEffect类的时候,我们有给实例添加一个scheduler属性,当我们在执行trigger的时候,我们可以选择是执行dep里的effect对象身上的run函数(也就是fn,也就是这里的getter函数),还是执行effect对象身上的scheduler方法。

image.png

在这里,我们选择执行scheduler方法。

为什么呢? 因为我们要保证,只有当访问我们需要的值(就是这里的cValue.value)的时候,我们才去执行getter函数。如果是直接执行run方法的话,那么当value.foo=2,value发生改变时,就会触发getter,我们并不希望这样。

所以添加一个scheduler方法就好啦!scheduler里面不需要去执行getter函数,并且我们还可以在scheduler里面,控制是否对当前的cValue 进行 读操作 (也就是 是否进入 get value()里面)。

添加代码之后,单测全部通过,但是还得想想为啥!再捋一捋

image.png

当响应式对象发生变化的时候,trigger执行的不是run()而是scheduler()。

======================================

认真想了一个晚上,终于又理清了一点逻辑。

这不得不回到之前的effect.ts 和 reactive.ts身上了。

首先可以明确的是,value.foo这个响应式对象关联的effect对象,是在computed.ts里面定义的这个_effect成员。因为我们想要触发value.foo的 scheduler() 而不是value.foo的run()

想了很久为什么 value.foo 关联的effect对象是定义在另一个ts文件里的?? 这就不得不回到track收集依赖的函数里去看了。

image.png

可以看到这里收集的是activeEffect对象,那么这个对象是啥?

image.png 可以看到activeEffect是一个全局变量,这个变量在effect实例执行run方法的时候被赋值。

被赋值成啥了?? 当然是run方法的调用者 ———当前effect实例了。

所以这个全局变量,是在effect实例第一次执行完run方法的之后,才会被赋值的。

当有了这个全局变量之后,我们对响应式对象进行proxy代理的时候,在触发get操作的时候,就可以通过track去收集这个当前这个响应式对象所依赖的effect实例也就是这个activeEffect全局变量了。

==============================================

接下来回到我们刚才的问题,value.foo 是怎么和定义在computed.ts里面的this._effect对象关联上的??

当然是执行了_effect.run()之后,初始化了全局对象activeEffect,然后在value.foo的get方法里收集到了这个全局对象activeEffect,之后在触发set操作的时候,就会去执行这个effect实例身上的scheduler方法啦!

关键还是在于这里。

image.png