Vue3 源码阅读(4):响应式系统 —— watch、computed

watch 和 computed 的实现都基于 ReactiveEffect 类,首先讲解 watch 的实现原理。

1,watch 的实现原理

1-1,watch api 的用法

watch 对应的官方 api 文档点击这里,watch api 的常见用法如下所示:

let obj = reactive({
  text: 'Hello'
})

watch(() => obj.text, (newVal, oldVal) => {
  console.log(`数据发生了变更,${oldVal} ==> ${newVal}`)
})

setTimeout(() => {
  obj.text = 'Vue' // 数据发生了变更,Hello ==> Vue
}, 1000)

在上面的代码中,watch api 接受了两个参数,第一个参数是一个 getter 函数,这个函数读取了一个响应式的属性,第二个参数是一个回调函数,当 obj.text 属性发生变化的时候,会触发执行这个回调函数,参数是 newVal 和 oldVal。 我们以上面的代码为例,进行 watch 实现原理的讲解。

1-2,watch 的底层实现原理

如果读懂了上一篇博客的话,watch 的实现原理是很容易想到了,我们可以利用 ReactiveEffect 类和 scheduler 函数实现 watch 的功能。

首先观察上面代码中的第一个参数,这个参数是一个函数,并且函数的执行进行了响应式数据的读取,我们可以把这个函数当成一个副作用函数,这个函数的执行能够触发响应式数据的依赖收集。watch api 第二个参数是一个回调函数,当读取的响应式数据发生了变化的话,要求触发执行这个回调函数。我们知道当响应式数据发生了变化的时候,在默认的情况下,Vue 会触发执行依赖的 run 方法,但是如果 ReactiveEffect 的实例上有 scheduler 函数的时候,Vue 则会触发执行这个 scheduler 函数,因此,我们可以在 scheduler 中进行回调函数的触发执行。

结合上面的思路,最简实现代码如下所示:

function watch(
  source,
  cb
): {
  doWatch(source, cb)
}

function doWatch(
  source,
  cb
) {
  let getter = source

  let oldValue
  let scheduler = () => {
    const newValue = effect.run()
    cb(newValue, oldValue)
    oldValue = newValue
  }

  const effect = new ReactiveEffect(getter, scheduler)
  // init run
  oldValue = effect.run()
}

1-3,监控整个 reactive 对象

watch 的第一个参数除了能是一个函数外,还可以是一个 reactive 对象,用法如下所示:

let obj = reactive({
  text: 'Hello'
})

watch(obj, (newVal, oldVal) => {
  console.log(`数据发生了变更,${oldVal} ==> ${newVal}`)
})

setTimeout(() => {
  obj.text = 'Vue' // 数据发生了变更,Hello ==> Vue
}, 1000)

此时,watch 会监控整个 reactive 对象,当对象中有任何一个属性发生了变化的话,都会执行回调函数,这个的实现原理其实非常简单,只需要改写一下 getter 即可。代码如下所示:

function doWatch(
  source,
  cb
) {
  let getter
  if(isReactive(source)){
    getter = () => traverse(source)
  } else {
    getter = source
  }
  
  let oldValue
  let scheduler = () => {
    const newValue = effect.run()
    cb(newValue, oldValue)
    oldValue = newValue
  }

  const effect = new ReactiveEffect(getter, scheduler)
  oldValue = effect.run()
}

// traverse 函数的作用是:遍历读取 value 中的属性
function traverse(value) {
  if (!isObject(value)) {
    return value
  } else {
    for (const key in value) {
      traverse(value[key])
    }
  }
}

新增改写的 getter 等于 () => traverse(source),traverse 函数的作用是遍历读取对象中的所有属性,这样,响应式对象中所有属性的 dep 都会对当前的 activeEffect 进行依赖收集,进而也就达到了监控整个 reactive 对象的目的。

watch api 还支持其他的特性,这里就不细说了。

2,computed 的实现原理

2-1,computed api 的用法

computed 的官方文档 api 点击这里,常见用法如下所示:

let obj = reactive({
  text: 'Hello'
})

let computedData = computed(() => `${obj.text},xxx`)

effect(() => {
  console.log("副作用函数执行")
  console.log(computedData.value)
})

setTimeout(() => {
  obj.text = 'Vue'
}, 1000)

// 输出如下所示:
// 副作用函数执行
// Hello,xxx
/ 1s后 //
// 副作用函数执行
// Vue,xxx

computed 函数的参数是一个 getter 函数,该 getter 函数会读取响应式数据,并返回一个值。computed 函数的返回值是一个 ref 值,我们可以在其他的 effect 中读取这个 ref 值,后续当 computed getter 所依赖的响应式数据发生变化时,会重新触发执行这些读取了 ref 值的 effect。

2-2,computed 的底层实现原理

computed 有以下几大特征。

  • getter 函数的执行时惰性的,当我们执行 computed 函数的时候,getter 函数并不会执行,只有当我们读取 ref 值的时候,getter 函数才会执行。
  • computed 具有缓存,当依赖的响应式数据没有变化的时候,computed 是不会重新计算的。
  • computed 函数的返回值是一个 ref。
  • 当其他的副作用函数读取 computed ref 值的时候,这个 computed ref 会进行依赖收集。
  • 当 getter 函数使用的响应式数据发生变化的时候,会触发执行依赖了 computed ref 的副作用函数重新执行。这些副作用函数重新执行,会读取 computed ref 值,这会触发 computed 的重新计算。

computed 的内部实现借助了 ReactiveEffect 类和访问器属性。这里,我直接给出实现的代码,然后进行代码讲解。

export class ComputedRefImpl {
  public dep?: Dep = undefined

  public readonly effect: ReactiveEffect

  public _dirty = true
  private _value!: T
  public readonly __v_isRef = true
  
  constructor(
    getter: ComputedGetter
  ) {
    this.effect = new ReactiveEffect(getter, () => {
      if (!this._dirty) {
        this._dirty = true
        triggerRefValue(this)
      }
    })
  }

  get value() {
    trackRefValue(this)
    if (this._dirty) {
      this._dirty = false
      this._value = this.effect.run()!
    }
    return this._value
  }
}

export function computed(
  getter
) {
  const cRef = new ComputedRefImpl(getter)
  return cRef
}

我们发现 computed 函数很简单,主要实现代码在 ComputedRefImpl 类中,首先讲解其内部几个属性的作用。

  • dep 属性,这个属性是以个 Set 类型的数据,用来存储(依赖收集)使用了当前计算属性的 effect。
  • _dirty:用来标识当前计算属性依赖的响应式数据有没有发生变化的,如果为 true 的话,这说明当前的计算属性需要重新计算,我们利用 ReactiveEffect 的 scheduler 函数检测计算属性依赖的响应式数据有没有发生变化。
  • _value:用来缓存计算属性值的。
  • __v_isRef:ref 值的一个标识变量。

上面四个核心属性如果理解了的话,整个 comuted 的实现原理也就基本都通了。

我们先看 constructor 函数,在构造器函数中,我们 new 了一个 ReactiveEffect 类的实例,第一个参数就是我们计算属性的 getter,当我们执行 effect.run() 的时候,计算属性就会进行值的重新计算,新值就是 run 函数的返回值。第二个参数是一个 scheduler 函数,当 getter 函数依赖的响应式数据发生变化的时候,这个调度函数就会被触发执行,在这里要做的事情是将 _dirty 属性设为 true,这标识着当前的计算属性需要重新计算求值,然后调用 triggerRefValue(this),这个函数的作用是重新执行依赖了当前响应式数据的副作用函数。

当我们执行依赖了计算属性值的副作用函数时,副作用函数会进行计算属性值的读取操作,也就是 computedDate.value,这个值是 ComputedRefImpl 类的一个访问器属性,这会触发类内部的 get value() 函数,这个函数的作用是返回最新的计算属性值,其内部首先进行计算属性值的依赖收集,然后判断 _dirty 是不是 true,如果为 true 的话,说明当前内部缓存的计算属性值不是最新的,需要执行 effect.run() 进行重新求值,计算出的最新值存放到 this._value 属性上,函数的最后 return this._value 即可。

3,总结

ok,Vue3 的 computed 和 watch 讲完了,下一篇博客,讲讲 Vue2 中的 computed 和 watch。

你可能感兴趣的:(vue3源码阅读系列,vue.js,前端,javascript)