likes
comments
collection
share

vue3.2 computed原理源码解析

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

我们知道computed的函数可接收两种传参,一种是函数,另一种是配置项包含get和set。 先来看一下基本的使用。

const name = ref('loookooo')
const sayOk = computed(()=>{
    console.log('computed')
    return name.value + ' say ok!'
})
effect(()=>{
    console.log('effect', sayOk.value)
})
name.value = 'hello'

执行结果

computed
effect,loookooo say ok!
computed
effect,hello say ok!

前两句是在effect函数执行时打印,当获取sayOk.value时,会进行依赖收集,将副作用绑定到sayOk上。并执行computed传进去的函数(副作用),此时打印 'computed', 接着获取name.value,name进行依赖收集,将computed传进去的函数绑定到name上。执行完毕,返回sayOk.value,回到effect副作用,打印'effect,loookooo say ok!'。 后两句是name.value被改变时打印,当更改name.value时,触发依赖,执行computed副作用(这里走的是调度器(scheduler)),在computed调度器中,会触发当前computed的依赖,也就是sayOk的依赖。对应effect副作用,此时又走获取sayOk.value的流程,只不过不会再去进行依赖收集了。 我们大概了解了基本的流程,接着我们翻出源码看看。 建议看完ref再来看computed (ref API我就不介绍了喔 在这里还有这里 ,effect 可以一看 ) ok 我们直接晒出computed

//getter定义
export type ComputedGetter<T> = (...args: any[]) => T
//setter定义
export type ComputedSetter<T> = (v: T) => void
//options定义
export interface WritableComputedOptions<T> {
  get: ComputedGetter<T>
  set: ComputedSetter<T>
}

export function computed<T>(
  getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>, //接收函数或者配置项
  debugOptions?: DebuggerOptions  //dev环境调试参数
) {
  let getter: ComputedGetter<T>
  let setter: ComputedSetter<T>
   //判断参数类别
  const onlyGetter = isFunction(getterOrOptions)
  if (onlyGetter) {
    //如果是参数则赋值给getter,setter为空函数
    getter = getterOrOptions
    setter = __DEV__
      ? () => {
          console.warn('Write operation failed: computed value is readonly')
        }
      : NOOP
  } else {
  //配置项则进行对应赋值
    getter = getterOrOptions.get
    setter = getterOrOptions.set
  }
  //创建ComputedRefImpl对象,并返回
  const cRef = new ComputedRefImpl(getter, setter, onlyGetter || !setter)
  //dev环境调试配置可忽略
  if (__DEV__ && debugOptions) {
    cRef.effect.onTrack = debugOptions.onTrack
    cRef.effect.onTrigger = debugOptions.onTrigger
  }
   
  return cRef as any
}

可以看到computed主要对传入的参数进行了区分处理,关键在于创建ComputedRefImpl对象,我们来看看这个类的实现。

class ComputedRefImpl<T> {
  public dep?: Dep = undefined

  private _value!: T  //值
  private _dirty = true //用于控制是否进行值更新(代表是否脏值)
  public readonly effect: ReactiveEffect<T>  //对应computed副作用,即getter

  public readonly __v_isRef = true //表示ref类型
  public readonly [ReactiveFlags.IS_READONLY]: boolean //是否只读

  constructor(
    getter: ComputedGetter<T>,
    private readonly _setter: ComputedSetter<T>,
    isReadonly: boolean
  ) {
  //根据getter创建响应式副作用,并设置调度器
    this.effect = new ReactiveEffect(getter, () => {
      //不是脏值则触发依赖
      if (!this._dirty) {
        this._dirty = true
        triggerRefValue(this)
      }
    })
    this[ReactiveFlags.IS_READONLY] = isReadonly
  }

  get value() {
    const self = toRaw(this) //获取原始值
    trackRefValue(self)  //依赖收集
    //脏值则进行更新
    if (self._dirty) {
      self._dirty = false
      self._value = self.effect.run()!
    }
    return self._value
  }

  set value(newValue: T) {
    this._setter(newValue)
  }
}

在构造函数中,对getter创建响应式副作用对象,并保存在effect属性上。 当获取值时,进行依赖收集,以及判断值是否需要进行更新,来触发绑定的副作用。


当effect()执行时, trackOpBit = 10, 执行fn, console.log('effect', sayOk.value),调用sayOk.get value() trackRefValue执行,初始化sayOk.dep = new Set(), n = 0, w = 0 newTracked() = false, 则 n = 10 wasTracked() = true, 则 shouldTrack = true 则进行依赖收集,sayOk.dep = [ ReactiveEffect ], ReactiveEffect.deps = [ dep ] fn执行完,回退trackOpBit = 1, n = 0, w = 0 判断if(self.dirty),_dirty默认值为true,进入判断 对dirty赋值false,防止再次取值时重复更新值 更新sayOk._value调用sayOk.effect.run(),与上面effect行为一致 执行fn,name.value时,进行依赖收集,将computed副作用与 name 进行绑定 此时,name.dep = [ sayOk.effect ], sayOk.effect.deps = [ name.dep ] 到这里则绑定成功,name与sayOk副作用互相绑定,而sayOk又与effect副作用互相绑定。


当我们去改变name.value时,则会触发对应依赖,即sayOk.effect 此时执行的是sayOk.effect.scheduler调度器,即一开始在构造函数中创建ReacitveEffect时传入的第二个参数。 this._dirty为false,进入判断。将_dirty设置为true,并调用tiriggerRefValue触发当前绑定依赖。 即effect副作用,当sayOk.value被获取时, 调用sayOk.get value(), 进行依赖收集,当然这里不会再进行收集啦,因为当前副作用已经绑定了该dep,再初始化标记时会对w标记与trackOpBit进行或运算赋值,在trackEffects中shouldTrack就为false啦。 这个时候_dirty为true,则会对_value进行更新,并返回。 这样,在effect副作用中拿到的sayOk就是最新的计算结果。


可以看到,computed创建一个ComputedRefImpl对象时,只有当这个对象的值被使用时,才会对传进入的函数进行执行分析,收集依赖。 而dirty则是用来对ComputedRefImpl._value是否需要进行更新的控制参数。保证在ComputedRefImpl.get value()被重复触发时,不会一直被更新而浪费性能。 只有当绑定了ComputedRefImpl.effect的响应式对象进行更新时,才会触发。