从源码角度深度解析computed计算属性

389 阅读7分钟

第一次在掘金上面分享经验,写着玩玩,纯属个人拙见,如有错误,多多包涵,还请指出,谢谢!

我用的这份源码呢是3.0.11,是很早的vue3的版本,后面新的vue源码呢我也看了一些,我们主要会用的源码大多都是小改小动,做了一些更好的封装。所以我认为目前我的这份老源码也是可以读的,哈哈哈哈

建议先看总结!!

首先是computed用法

相信我们都知道,computed最常用的用法呢就是传入一个getter
const num = ref(1)
const counter = computed(() => num.value + 1)

console.log(counter.value) // 2

counter.value++ // 错误
//如果我们没有给computed传入第二个参数呢(setter)是不能对其修改的(后面源码中会有所体现)
但是呢,使用computed时,还可以传入第二个参数setter
const num = ref(1)
const counter = computed({
  get: () => num.value + 1,
  set: (val) => {
    num.value = val - 1
  },
})

counter.value = 1
console.log(num.value) // 0
//可能这个例子有点莫名其妙,哈哈哈哈,但事实上就是可以这样用

直接正题,甩出computed源码

compted源码呢,是在packages/reactivity/src/computed.ts

export function computed<T>(getter: ComputedGetter<T>): ComputedRef<T>
export function computed<T>(
  options: WritableComputedOptions<T>
): WritableComputedRef<T>
export function computed<T>(
  getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>
) {
//这里可以稍加注意
//首先判断我们传入的是不是一个函数(如果是,那么就是我们平时常用的用法,只传入一个getter),此时把setter设置为NOOP
  let getter: ComputedGetter<T>
  let setter: ComputedSetter<T>

  if (isFunction(getterOrOptions)) {
    getter = getterOrOptions
    setter = __DEV__
      ? () => {
          console.warn('Write operation failed: computed value is readonly')
        }
      : NOOP
  } else {
  //如果我们是传入的对象(也就是getter和setter一起传入),那么直接设置
    getter = getterOrOptions.get
    setter = getterOrOptions.set
  }

  return new ComputedRefImpl(
    getter,
    setter,
    isFunction(getterOrOptions) || !getterOrOptions.set
    //这里其实就是判断是否传入setter
  ) as any
}

这里的源码很简单,只要我们平时用熟了computed,相信这里稍加揣摩,就能看懂。是不是想说一句,computed也就那样,没啥好特别的,它的实现函数不就是接受一个getter和setter吗,然后new一个ComputedRefImpl,不过如此而已。

哈哈哈哈,前面的代码真的好简单,但是下面这个ComputedRefImpl类,还是有我们值得研究的地方,嘻嘻
下面的代码有一点点绕,我标注了解读步骤
class ComputedRefImpl<T> {
  private _value!: T
  private _dirty = true

  public readonly effect: ReactiveEffect<T>

  public readonly __v_isRef = true;
  public readonly [ReactiveFlags.IS_READONLY]: boolean

  constructor(
    getter: ComputedGetter<T>,
    private readonly _setter: ComputedSetter<T>,
    isReadonly: boolean
  ) {
  //第一步
  //我靠,不是吧,一来就调用了响应式副作用函数,真有你的
  //响应式副作用函数:传入第一个参数是函数,传入第二个参数是配置对象
  //这里简单说明一下,因为effect函数也有很多值得思考的地方
  
  //第一步
  //首先调用了effect函数,而effect函数内部呢会调用用createReactiveEffect,
  //也就是const effect = createReactiveEffect(fn, options),紧接着就是在
  //内部调用这个返回effect函数。
  
  //本质上就是执行一次fn,并且把fn置为待收集依赖函数,而在执行fn过程中会访问fn内部
  //用到的响应式数据,这个响应式数据就会收集自己和待依赖函数之间的依赖
  
  //这个时候就建立起了依赖关系,也就是fn这个函数和fn函数里面用到的响应式数据之间的关系,
  //此后fn函数里的响应式数据一发生改变,那么就会执行options中的scheduler(一般情况下)
  
  //第一步
  //但是如果我们在options中传入lazy: true,那么这个createReactiveEffect返回的
  //effect函数就不会立刻执行。所以下面的代码,此时此刻还未建立起getter和getter内部的
  //响应式数据之间依赖关系
  //那么会在何时建立依赖关系呢,请看后面的代码解读

    this.effect = effect(getter, {
      lazy: true,
      scheduler: () => {
      //我们都知道computed是有缓存性的,就是通过这个dirty实现的,请看后面的代码解读
        if (!this._dirty) {
          this._dirty = true
          //第三步
          //这里的trigger函数就是触发的第二步中建立的依赖关系,而这个trigger函数是在
          //scheduler中,也就是说未来我们computed值所依赖的响应式数据发生变化时,那
          //么会执行到这里,把dirty置为true(依赖值改变,置为true,重新计算)并且触发trigger
          //而trigger出发了,那么组件更新函数执行 => 渲染函数执行 => 访问computed值
          //=> 触发computed的get value() => 执行this.effect拿到最新computed值
          
          //这里我们会有一个疑问,为什么computed依赖的响应式数据发生改变,不直接在
          //scheduler中直接计算出最新值,而是触发trigger,让渲染函数执行时去触发
          //computed的get操作,再在其中执行this.effect来计算出最新值。
          //因为我们computed值可能会依赖多个响应式数据,所以在这里先把dirty设为true,
          //之后访问computed值的时候对其修改是最合适的。
          
          trigger(toRaw(this), TriggerOpTypes.SET, 'value')
        }
      }
    })

    this[ReactiveFlags.IS_READONLY] = isReadonly
  }


  get value() {
    // the computed ref may get wrapped by other proxies e.g. readonly() #3376
    //第二步
    //我们知道我们写的vue代码里面,template模版里的代码经过vue编译器一顿乱操作,
    //就会被编译成渲染函。在后续的组件更新函数中,会执行这个渲染函数,拿到虚拟dom,
    //再进行patch操作(你不知道,我也默认你知道!!!吼!!!)
    //那么再执行这个编译好的渲染函数时,因为要生成vdom,所以就会访问到我们所定义的
    //computed数据,此时此刻就会触发这个get函数。
    const self = toRaw(this)
    //第二步
    //这个dirty默认值是true(前面可以看到),那么紧接着就会执行this.effect,此时
    //此刻就建立起了前面所说的依赖关系,而且而且还拿到返回值,也就是计算之后的computed
    //值。并且把dirty置为false,也就是我们所说的缓存性,下次再访问这个computed值的
    //时候呢,如果dirty时false,直接返回,不做计算。
    //那么什么时候会将dirty置为true呢,请继续看
    if (self._dirty) {
      self._value = this.effect()
      self._dirty = false
    }
    
   //第二步
   //咦,这里有个track耶,track不是收集依赖吗,传入的是self耶,那么他是收集self
   //和谁的依赖呢?我靠真是晕头转向的
   
   //第一次执行组件更新函数时,也是调用了effect函数的,那么此时此刻收集的
   //依赖就是组件更新函数和此computed数据之间的依赖关系,那么会在何时触发依赖呢,请看第三步
    track(self, TrackOpTypes.GET, 'value')
    return self._value
  }

  set value(newValue: T) {
  //这里我们可以看出如果我们使用computed没有传入setter的话,setter默认值时NOOP,调用就会报错
    this._setter(newValue)
  }
}

总结

前面的注解实在是有一些绕,这里我做一个简单的总结

首先我们要理解effect函数是干嘛的,调用effect函数会传入fn和option。如果option里面没有配置lazy:true的话,默认effect会执行,本质上就是执行fn并且将fn置为待收集依赖函数,而fn执行过程中会访问响应式数据,响应式数据在被访问时会收集自己跟待收集函数之间的依赖关系,在响应式数据被改变时会触发依赖,执行所收集的函数。

computed的本质呢,是在第一次访问computed值的时候建立起compuetd值和所依赖的响应式数据之间的依赖。什么时候触发此依赖呢,就是在响应式数据改变之时。

而computed在第一次被访问时又会收集自己和访问自己的组件更新函数之间的依赖关系,什么时候又触发这个这个依赖呢,是在前面响应式依赖被触发时,并且把dirty值置为true(computed缓存性)

响应式数据改变 => 触发响应式数据和computed之间的依赖 => dirty=true,触发computed所依赖的组件更新函数 => 组件更新函数内部执行渲染函数,会访问computed值 => 计算最新的computed值(因为dirty为true),并把dirty设为false

当computed所依赖的响应式数据未发生改变时,此时dirty是false,那么此时去访问computed的时候就就不会重新计算computed的值,因此实现了computed的缓存性。

希望对你有帮助