原来Vue3中的Computed也没有多么神奇

89 阅读5分钟

分享:我欲与君相知,长命无绝衰。山无棱,江水为竭,冬雷震震,夏雨雪,天地合,乃敢与君绝!——《上邪》

以后的每篇都分享一下喜欢的文字(小说段落,诗词等等,兄弟萌有喜欢的也可以给我分享一下!)

前言

相信各位大佬在使用Vue的时候都用过Computed 计算属性这个响应式核心API,那么我们对它的奇妙之处,神奇之处了解多少呢?想要搞清楚它是什么,首先就需要知道它的神奇之处(Vue2与Vue3实现有所不同,以Vue3为主) ps:Vue2源码没看😏:

  1. 它的返回值是一个响应式ref对象
  2. 它接收一个getter函数,具有缓存性,当依赖没改变的时候会执行返回缓存值,依赖改变会重新计算再返回并且缓存当前值

只要我们搞懂了这两点,Computed岂不是手到擒来

“神奇”的功能性

我们知道了计算属性的神奇,想想如何实现这样的“神奇”,所以我们先用测试代码写出我们要实现出来的功能,然后再根据功能实现代码

  1. 返回值:响应式ref对象
const 陪我去看海 = reactive({
  age: 22,
});
const age = computed(() => {
  return 陪我去看海.age;
});
expect(age.value).toBe(22);
  1. 缓存性
const value = reactive({
  foo: 22,
});
const getter = jest.fn(() => {
  return value.foo;
});
// 当数据被get(获取)的时候,执行一次getter,多次get,也执行一次
const cValue = computed(getter);
expect(getter).not.toHaveBeenCalled();
expect(cValue.value).toBe(22);
expect(getter).toHaveBeenCalledTimes(1);
cValue.value;
expect(getter).toHaveBeenCalledTimes(1);

// 返回的是响应式对象,根据响应式原理,set的时候触发依赖函数,这里依赖函数的主体就是getter
value.foo = 23;
expect(getter).toHaveBeenCalledTimes(1);
expect(cValue.value).toBe(23);
expect(getter).toHaveBeenCalledTimes(2);
cValue.value;
expect(getter).toHaveBeenCalledTimes(2);

“神奇”的实现

  1. 返回ref对象
class ComputedRefImpl {
  private _getter: any;
  private _value: any;
  constructor(getter) {
    this._getter = getter;
  }

  get value() {
    this._value = this._getter();
    return this._value;
  }
}

export function computed(getter) {
  return new ComputedRefImpl(getter);
}

computed是一个函数,参数也是一个函数,内部实现使用一个class实现,让它返回一个类的实例,computed的返回值,就是getter函数的返回值,当用户传递函数进来后,使用 _getter 属性进行存储,使用 _value 属性进行存储值,最后当每次用户调用get value的时候取到值,相当于模拟ref的使用方法。

  1. 缓存性
class ComputedRefImpl {
  private _getter: any;
  private _dirty: boolean = true;
  private _value: any;
  private _effect: any;
  constructor(getter) {
    this._getter = getter;
    // 这里执行顺序,先schedule再getter,也就是在set的时候,先打开开关,再执行getter
    this._effect = new ReactiveEffect(getter, () => {
      if (!this._dirty) {
        this._dirty = true;
      }
    });
  }

  get value() {
    /* 
        当值没有改变,直接取存储的值
        一直调用get,不会重新计算,只有在set后才会重新计算值
    */
    if (this._dirty) {
      this._dirty = false;
      this._value = this._effect.run();
    }
    return this._value;
  }
}

export function computed(getter) {
  return new ComputedRefImpl(getter);
}

PS:这里 class ReactiveEffect 先理解成相当于包装一下这个函数,让它被收集进Dep,执行run的时候就是执行它自己,后面会讲到,暂时这样理解就行

这里我们新增了两个私有属性 _dirty, _effect

  • dirty是用来作为是否需要重新计算的开关;
  • effect是用来将用户传入进来的函数通过ReactiveEffect包装变为依赖函数并存储;

这里也就是说,当第一次被get的时候,它dirty初始值是true,所以执行getter关闭dirty,让后面的get取值的时候取的是 _value 存储的值,这里就实现了缓存的特性。但是问题就是当被set后,我们需要更新后的值,所以当set的时候,我们需要打开 _dirty 让其重新计算,这里我们是使用到了另外一个类ReactiveEffect来实现。接下来看看它主要实现是如何的

class ReactiveEffect

// 用来包装依赖函数的类
export class ReactiveEffect {
  private _fn;
  public scheduler: Function | undefined;
  constructor(fn, scheduler?: Function) {
    this._fn = fn;
    this.scheduler = scheduler;
  }
  run() {
    activeEffect = this;
    // 在这里会执行 track
    const result = this._fn();
    // 返回 runner 执行后的值
    return result;
  }
}

可以看到,这里其实就是相当于给getter函数穿了一层衣服,主要目的就是为了这个scheduler,通过这个来改变是否需要重新计算的状态,以及把函数收集进入依赖容器,为后面set的时候执行scheduler埋下伏笔,这里响应式逻辑就把相关的逻辑挑出来看下了(详细得自己去了解一下,或者看一下我的另外一篇文章),了解响应式就了解两个逻辑就好(如何收集?如何执行?)

如何收集?如何执行?

  • 如何收集?
export function trackEffect(dep) {
  if (dep.has(activeEffect)) return;
  dep.add(activeEffect);
  // dep 存 activeEffect, activeEffect反向存储dep
  activeEffect.deps.push(dep);
}

这里 dep 就是一个容器,至于具体是什么,不用管,他的目的就是将 activeEffect 收集进 dep 中存储,该函数会在get的时候执行

  • 如何执行?
export function triggerEffects(dep) {
  // 注意:这里是对象遍历,所以这两个属性如果有,都会执行,只有先后顺序
  for (const effect of dep) {
    if (effect.scheduler) {
      effect.scheduler();
    } else {
      effect.run();
    }
  }
}

dep就是那个收集依赖的容器,然后遍历这个容器取出所有相关依赖对象,当有scheduler的时候先执行scheduler,没有的时候直接执行run也就是函数本身,该函数会在set的时候执行

这样后,我们再把前后的知识串联起来看,就实现出了一个简易版的Computed

知识串联总结

  1. computed 内部,在取值的时候会有一个开关,true的时候,重新计算了值,并存储到一个私有属性上,false的时候,直接取上一次存储进去的值(必然会有一次执行,那就是初始化)
  2. 开关更改的时机只有一个地方,那就是在set更新值之后,将开关打开,在下一次get的时候重新计算
  3. set的时候都会执行一个trigger函数,该函数会把收集起来的对应依赖遍历执行,在执行的时候会先执行scheduler,然后执行函数本身

这个功能属于Vue中响应式核心的一部分,所以如果想要更好的明白细节的话,需要先了解响应式原理。