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


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

    1,watch 的实现原理

    1-1,watch api 的用法

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

    1. let obj = reactive({
    2. text: 'Hello'
    3. })
    4. watch(() => obj.text, (newVal, oldVal) => {
    5. console.log(`数据发生了变更,${oldVal} ==> ${newVal}`)
    6. })
    7. setTimeout(() => {
    8. obj.text = 'Vue' // 数据发生了变更,Hello ==> Vue
    9. }, 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 中进行回调函数的触发执行。

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

    1. function watch(
    2. source,
    3. cb
    4. ): {
    5. doWatch(source, cb)
    6. }
    7. function doWatch(
    8. source,
    9. cb
    10. ) {
    11. let getter = source
    12. let oldValue
    13. let scheduler = () => {
    14. const newValue = effect.run()
    15. cb(newValue, oldValue)
    16. oldValue = newValue
    17. }
    18. const effect = new ReactiveEffect(getter, scheduler)
    19. // init run
    20. oldValue = effect.run()
    21. }

    1-3,监控整个 reactive 对象

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

    1. let obj = reactive({
    2. text: 'Hello'
    3. })
    4. watch(obj, (newVal, oldVal) => {
    5. console.log(`数据发生了变更,${oldVal} ==> ${newVal}`)
    6. })
    7. setTimeout(() => {
    8. obj.text = 'Vue' // 数据发生了变更,Hello ==> Vue
    9. }, 1000)

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

    1. function doWatch(
    2. source,
    3. cb
    4. ) {
    5. let getter
    6. if(isReactive(source)){
    7. getter = () => traverse(source)
    8. } else {
    9. getter = source
    10. }
    11. let oldValue
    12. let scheduler = () => {
    13. const newValue = effect.run()
    14. cb(newValue, oldValue)
    15. oldValue = newValue
    16. }
    17. const effect = new ReactiveEffect(getter, scheduler)
    18. oldValue = effect.run()
    19. }
    20. // traverse 函数的作用是:遍历读取 value 中的属性
    21. function traverse(value) {
    22. if (!isObject(value)) {
    23. return value
    24. } else {
    25. for (const key in value) {
    26. traverse(value[key])
    27. }
    28. }
    29. }

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

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

    2,computed 的实现原理

    2-1,computed api 的用法

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

    1. let obj = reactive({
    2. text: 'Hello'
    3. })
    4. let computedData = computed(() => `${obj.text},xxx`)
    5. effect(() => {
    6. console.log("副作用函数执行")
    7. console.log(computedData.value)
    8. })
    9. setTimeout(() => {
    10. obj.text = 'Vue'
    11. }, 1000)
    12. // 输出如下所示:
    13. // 副作用函数执行
    14. // Hello,xxx
    15. / 1s后 //
    16. // 副作用函数执行
    17. // 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 类和访问器属性。这里,我直接给出实现的代码,然后进行代码讲解。

    1. export class ComputedRefImpl {
    2. public dep?: Dep = undefined
    3. public readonly effect: ReactiveEffect
    4. public _dirty = true
    5. private _value!: T
    6. public readonly __v_isRef = true
    7. constructor(
    8. getter: ComputedGetter
    9. ) {
    10. this.effect = new ReactiveEffect(getter, () => {
    11. if (!this._dirty) {
    12. this._dirty = true
    13. triggerRefValue(this)
    14. }
    15. })
    16. }
    17. get value() {
    18. trackRefValue(this)
    19. if (this._dirty) {
    20. this._dirty = false
    21. this._value = this.effect.run()!
    22. }
    23. return this._value
    24. }
    25. }
    26. export function computed(
    27. getter
    28. ) {
    29. const cRef = new ComputedRefImpl(getter)
    30. return cRef
    31. }

    我们发现 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。

  • 相关阅读:
    mybatis分页插件PageHelper导致自定义拦截器失效
    VSCode的有用插件
    Java - ConcurrentHashMap原理分析
    nmap参数详解
    一天完成react面试准备
    电脑不小心删除的文件怎么恢复?
    基于C#和OpenVINO在英特尔独立显卡上部署PP-TinyPose模型
    陕西灵活用工平台开发有风险吗?
    【无需卸载,丝滑关闭奇安信天擎开机自启动(步骤超简单)】
    mmclassification 训练自定义数据
  • 原文地址:https://blog.csdn.net/f18855666661/article/details/126284864