【vue3源码】四、computed源码解析

【vue3源码】四、computed源码解析

参考代码版本:vue 3.2.37

官方文档:https://vuejs.org/

计算属性。接受一个getter函数,并根据getter函数的返回值返回一个不可变的响应式ref对象。或者,接受一个具有getset函数的对象,用来创建可写的ref对象。
文件位置:packages/reactivity/src/computed.ts

使用示例

只读的计算属性:

const count = ref(1)
const doubleCount = computed(() => count.value * 2)

console.log(doubleCount.value) // 2

count.value = 2
console.log(doubleCount.value) // 3

可写的计算属性:

const count = ref(1)
const plusOne = computed({
  get: () => count.value + 1,
  set: (val) => {
    count.value = val - 1
  }
})

console.log(plusOne.value) // 2

plusOne.value = 1
console.log(count.value) // 0

进行调试:

const count = ref(1)
const doubleCount = computed(() => count.value * 2, {
  onTrack(e) {
    console.log('track')
    console.log(e)
  },
  onTrigger(e) {
    console.log('trigger')
    console.log(e)
  }
})

// 触发track监听
console.log(doubleCount.value)

// 触发trigger监听
count.value++

源码

export function computed<T>(
  getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>,
  debugOptions?: DebuggerOptions,
  isSSR = false
) {
  let getter: ComputedGetter<T>
  let setter: ComputedSetter<T>

  const onlyGetter = isFunction(getterOrOptions)
  if (onlyGetter) {
    getter = getterOrOptions
    setter = __DEV__
      ? () => {
          console.warn('Write operation failed: computed value is readonly')
        }
      : NOOP
  } else {
    getter = getterOrOptions.get
    setter = getterOrOptions.set
  }

  const cRef = new ComputedRefImpl(getter, setter, onlyGetter || !setter, isSSR)

  if (__DEV__ && debugOptions && !isSSR) {
    cRef.effect.onTrack = debugOptions.onTrack
    cRef.effect.onTrigger = debugOptions.onTrigger
  }

  return cRef as any
}

computed接收两个参数,第二参数是依赖收集和触发依赖的钩子函数,只在开发环境中起作用,这里就不做解释了。主要看第一个参数,观察其类型,发现可以传两种参数:一种是一个getter函数,一种是个包含getset的对象。

首先从getterOrOptions中确定gettersetter(如果getterOrOptions是个function,说明computed是不可写的,所以会将setter设置为一个空函数),确定好之后,创建一个ComputedRefImpl实例,并将其返回。

ComputedRefImpl

export class ComputedRefImpl<T> {
  public dep?: Dep = undefined
  // 缓存的值
  private _value!: T
  // 在构造器中创建的ReactiveEffect实例
  public readonly effect: ReactiveEffect<T>

  // 标记为一个ref类型
  public readonly __v_isRef = true
  // 只读标识
  public readonly [ReactiveFlags.IS_READONLY]: boolean

  // 是否为脏数据,如果是脏数据需要重新计算
  public _dirty = true
  // 是否可缓存,取决于SSR
  public _cacheable: boolean

  constructor(
    getter: ComputedGetter<T>,
    private readonly _setter: ComputedSetter<T>,
    isReadonly: boolean,
    isSSR: boolean
  ) {
    this.effect = new ReactiveEffect(getter, () => {
      if (!this._dirty) {
        this._dirty = true
        triggerRefValue(this)
      }
    })
    this.effect.computed = this
    this.effect.active = this._cacheable = !isSSR
    this[ReactiveFlags.IS_READONLY] = isReadonly
  }

  get value() {
    // computed可能被其他proxy包裹,如readonly(computed(() => foo.bar)),所以要获取this的原始对象
    const self = toRaw(this)
    // 收集依赖
    trackRefValue(self)
    // 如果是脏数据或者是SSR,需要重新计算
    if (self._dirty || !self._cacheable) {
      // _dirty取false,防止依赖不变重复计算
      self._dirty = false
      // 计算
      self._value = self.effect.run()!
    }
    return self._value
  }

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

构造器

ComputedRefImpl构造器接收四个参数:gettersetterisReadonly(是否只读)、isSSR(是否为SSR)。

constructor(
  getter: ComputedGetter<T>,
  private readonly _setter: ComputedSetter<T>,
  isReadonly: boolean,
  isSSR: boolean
) {
  this.effect = new ReactiveEffect(getter, () => {
    if (!this._dirty) {
      this._dirty = true
      triggerRefValue(this)
    }
  })
  // this.effect.computed指向this
  this.effect.computed = this
  // this.effect.active与this._cacheable在SSR中为false
  this.effect.active = this._cacheable = !isSSR
  this[ReactiveFlags.IS_READONLY] = isReadonly
}

在构造器中声明了一个ReactiveEffect,并将getter和一个调度函数作为参数传入,在调度器中如果_dirtyfalse,会将_dirty设置为true,并执行triggerRefValue函数。

triggerRefValue可以接受两个值:refnewVal

export function triggerRefValue(ref: RefBase<any>, newVal?: any) {
  ref = toRaw(ref)
  if (ref.dep) {
    if (__DEV__) {
      triggerEffects(ref.dep, {
        target: ref,
        type: TriggerOpTypes.SET,
        key: 'value',
        newValue: newVal
      })
    } else {
      triggerEffects(ref.dep)
    }
  }
}

triggerRefValue中首先获取ref的原始对象,如果ref的原始对象中有dep属性,则触发dep中的依赖。

在初始化effect之后,会将这个effect赋给ComputedRefImpl实例的effect属性,并将effect.computed指向ComputedRefImpl实例

value取值函数

get value() {
  // computed可能被其他proxy包裹,如readonly(computed(() => foo.bar)),所以要获取this的原始对象
  const self = toRaw(this)
  // 收集依赖
  trackRefValue(self)
  // 如果是脏数据,需要重新计算
  if (self._dirty || !self._cacheable) {
    // _dirty取false,防止依赖不变重复计算
    self._dirty = false
    // 计算
    self._value = self.effect.run()!
  }
  return self._value
}

当读取ComputedRefImpl实例的value属性时,由于计算属性可能被其他proxy包裹,所以需要使用toRaw获取其原始对象。

const self = toRaw(this)

然后调用trackRefValue进行依赖的收集。

export function trackRefValue(ref: RefBase<any>) {
  // 如果允许收集并且存在activeEffect进行依赖收集
  if (shouldTrack && activeEffect) {
    ref = toRaw(ref)
    if (__DEV__) {
      trackEffects(ref.dep || (ref.dep = createDep()), {
        target: ref,
        type: TrackOpTypes.GET,
        key: 'value'
      })
    } else {
      trackEffects(ref.dep || (ref.dep = createDep()))
    }
  }
}

接着根据_dirty_cacheable属性来决定是否需要修改self._value,其中_dirty表示是否为脏数据,_cacheable表示是否可以缓存(取决于是否为服务端渲染,如果为服务端渲染则不可以缓存)。如果是脏数据或不可以被缓存,那么会将_dirty设置为false,并调用self.effect.run(),修改self._value

if (self._dirty || !self._cacheable) {
  self._dirty = false
  self._value = self.effect.run()!
}

最后返回self._value

return self._value

value存值函数

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

当修改ComputedRefImpl实例的value属性时,会调用实例的_setter函数。

到此,你会发现computed是懒惰的,只有使用到computed的返回结果,才能触发相关计算。

为了加深对computed的理解,接下来以一个例子分析computed的缓存及计算过程:

const value = reactive({ foo: 1 })
const cValue = computed(() => value.foo)
console.log(cValue.value) // 1

value.foo = 2
console.log(cValue.value) // 2

当打印cValue.value时,会命中ComputedRefImpl对应的get方法,在get中,执行trackRefValue收集对应依赖(由于此时没有处于活跃状态的effect,即activeEffect,所以并不会进行依赖的收集),默认_dirtytrue,将_dirty设置为false,并执行effect.run,计算数据,计算完成后将数据缓存至selft._vlaue中,方便下次的利用。在调用effect.run过程中,会将在ComputedRefImpl构造器中创建的ReactiveEffect实例收集到targetMap[toRaw(value)].foo中。

当修改value.foo = 2,触发targetMap[toRaw(value)].foo中的依赖,由于在初始化ReactiveEffect时,设置了一个调度器,所以在触发依赖过程中会执行这个调度器。这个调度器中会判断如果_dirty===false,则将_dirty设置为true,并手动调用triggerRefValue触发依赖,在调用triggerRefValue的过程中,因为cValue.dep=undefined,所以没有依赖要触发。

当第二次打印cValue.value时,由于_dirtytrue,所以会执行cValue.effect.run,并将结果赋值给cValue._value,最后返回cValue._value,打印结果2

总结

computed本质也是个refComputedRefImpl),它是懒惰的,如果不使用计算属性,那么是不会进行计算的,只有使用它,才会调用计算属性中的effect.run方法进行计算,同时将结果缓存到_value中。

computed如何重新计算?

首先在第一次获取计算属性的值的过程中会进行依赖的收集,假设计算属性的计算与响应式对象的a、b两个属性有关,那么会将computed中生成的ReactiveEffect实例收集到targetMap[obj].atargetMap[obj].b中,一旦ab属性变化了,会触发依赖,而在依赖的触发过程中会执行调度函数,在调度函数中会将脏数据的标识_dirty设置为true,并触发计算属性的依赖。那么在下一次使用到计算属性的话,由于_dirtytrue,便会调用计算属性中的effect.run方法重新计算值。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345

推荐阅读更多精彩内容