vue3源码系列(三)——computed篇

104 阅读3分钟

介绍computed

跟之前一样,在介绍computed源码之前还是先说一下他的用法,和具体使用场景。

  1. computed被称为计算属性,顾名思义,在碰到一些需要大量计算的业务场景的时候我们会选择他,如一些购物车之类的。
  2. computed只会在相关响应式依赖发生改变时才会重新求值。如果响应式数据发生变化,则computed会依据它所依赖的数据进行重新计算。
  3. computed具备有缓存机制的。 在看源码之前,先看一个使用computed的例子:
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
    <body>

      <div id="app"></div>

      <script src="./reactivity.global.js"></script>

      <script>

        const { reactive, computed, effect } = VueReactivity

        const countInfo = reactive({count: 1})

        const personal = computed(() => countInfo.count + 1)

        effect(() => {
          document.getElementById('app').innerHTML = `数字为: ${personal.value}`
        })

        setTimeout(() => {
          countInfo.count += 1
        }, 2000)

      </script>
    </body>
</html>

如上代码页面一开始显示2,两秒后变为3。这说明computed在其依赖项改变之后会重新计算并触发试图更新,并且在访问computed属性是需要额外加一层.value。

computed源码

computed其实是个effect函数,带着这个结论会更容易理解computed的执行机制。


import { ReactiveEffect, trackEffects, triggerEffects } from "./effect";// 引入的这些函数都是前几篇介绍了的

export const computed = (getterOrOption) => {
    let onlyGetter = isFunction(getterOrOption); // 判断传进来的是不是函数(computed接受函数或者对象作为参数)
    let getter; // 访问计算属性值的方法
    let setter; // 设置计算属性值得方法
    if(onlyGetter) {
        getter = getterOrOption;
        setter = () => {console.log('no set')}
    }else {
        getter = getterOrOption.get;
        setter = getterOrOption.set;
    }

    return new ComputedRefImpl(getter, setter); // 重点: computed返回一个ComputedRefImpl
}


class ComputedRefImpl{
    public effect;
    public _dirty = true; // 缓存机制标识,默认应该取值的时刻进行计算
    public _value;
    public dep = new Set; // 储存计算属性外层的effect依赖
    constructor(getter, public setter) {
        // 我们将用户的getter放到effect中,这里面的属性就会被effect收集起来
        this.effect = new ReactiveEffect(getter, () => {
            // 属性发生变化,就会执行调度
            if(!this._dirty) {
                this._dirty = true;
                triggerEffects(this.dep)
            }
        } // 该函数就是个调度器-scheduler
        )
    }
    get value() {
        // 要有依赖收集的能力
        trackEffects(this.dep); // 计算属性收集外层的effect,用于计算属性改变触发effect更新

        if(this._dirty) {
            this._dirty = false;
            this._value = this.effect.run();
        }
        return this._value;
    }
    set value(newValue) {
            this.setter(newValue); // 赋值时走该方法
    }
}

如上代码可知,在computed方法中通过ComputedRefImpl类实例化一个对象,当我们在effect中使用计算属性时,就会触发get方法,触发trackEffects就行依赖收集。这是_dirty为true,即会运行effect的run方法。这个方法我们前面讲reactive时就讲过,在run方法中会运行fn方法(这里的getter),并返回fn方法的返回值。在执行fn方法时,又会访问响应式数据(这就是computed的依赖数据必须是响应式的原因),此时会进行依赖收集,而收集到的effect就是当前的computed。当依赖的响应式数据改变了,会触发更新,执行triggerEffects,这是的dep里收集的是computed。在scheduler里又会执行triggerEffects,但这时的dep是computed的外层的effect,所以就触发了视图的更新。

总结:

计算属性依赖的数据必须是响应式的。依赖项变化能触发计算属性重新计算的原理就是依赖数据把computed当作effect一样收集起来,当其值发生改变时,会触发收集的effect(computed)执行,在执行过程中又会执行triggerEffects方法,继而执行计算属性收集的外层effect,最终触发了试图更新。