vue3 computed 探究

217 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第5天,点击查看活动详情

计算属性是 Vue.js 开发中一个非常实用的 API ,它允许用户定义一个计算方法,然后根据一些依赖的响应式数据计算出新值并返回。当依赖发生变化时,计算属性可以自动重新计算获取新值,所以使用起来非常方便。

看一个例子:

const count = ref(1) 

const plusOne = computed(() => count.value + 1) 
console.log(plusOne.value) // 2 

plusOne.value++ // error 
count.value++ 
console.log(plusOne.value) // 3

从代码中可以看到,我们先使用 ref API 创建了一个响应式对象 count,然后使用 computed API 创建了另一个响应式对象 plusOne,它的值是 count.value + 1,当我们修改 count.value 的时候, plusOne.value 就会自动发生变化。

注意,这里我们直接修改 plusOne.value 会报一个错误,这是因为如果我们传递给 computed 的是一个函数,那么这就是一个 getter 函数,我们只能获取它的值,而不能直接修改它。

在 getter 函数中,我们会根据响应式对象重新计算出新的值,这也就是它被叫做计算属性的原因,而这个响应式对象,就是计算属性的依赖。

当然,有时候我们也希望能够直接修改 computed 的返回值,那么我们可以给 computed 传入一个对象:

const count = ref(1) 

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

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

我们给 computed 函数传入了一个拥有 getter 函数和 setter 函数的对象,getter 函数和之前一样,还是返回 count.value + 1;而 setter 函数,请注意,这里我们修改 plusOne.value 的值就会触发 setter 函数,其实 setter 函数内部实际上会根据传入的参数修改计算属性的依赖值 count.value,因为一旦依赖的值被修改了,我们再去获取计算属性就会重新执行一遍 getter,所以这样获取的值也就发生了变化。

上面我们看到了computed的两种使用方式,同时也知道了两点:

  1. 不能直接修改computed的值
  2. computed的返回值不能在组件的data中出现

那它是怎么被实现的呢?

function computed(getterOrOptions) { 
    // getter 函数 
    let getter 

    // setter 函数 
    let setter 

    // 标准化参数 
    if (isFunction(getterOrOptions)) { 
        // 表面传入的是 getter 函数,不能修改计算属性的值 
        getter = getterOrOptions 
        setter = (process.env.NODE_ENV !== 'production') 
            ? () => { 
                console.warn('Write operation failed: computed value is readonly') 
            } 
            : NOOP 
    } 
    else { 
        getter = getterOrOptions.get 
        setter = getterOrOptions.set 
    } 

    // 数据是否脏的 
    let dirty = true 

    // 计算结果 
    let value
    let computed

    // 创建副作用函数 
    const runner = effect(getter, { 
        // 延时执行 
        lazy: true, 
        // 标记这是一个 computed effect 用于在 trigger 阶段的优先级排序 
        computed: true, 

        // 调度执行的实现 
        scheduler: () => { 
            if (!dirty) { 
                dirty = true 
                // 派发通知,通知运行访问该计算属性的 activeEffect 
                trigger(computed, "set" /* SET */, 'value') 
            } 
        }
    }) 

    // 创建 computed 对象 
    computed = { 
        __v_isRef: true, 
        // 暴露 effect 对象以便计算属性可以停止计算 
        effect: runner, 
        get value() { 
            // 计算属性的 getter 
            if (dirty) { 
                // 只有数据为脏的时候才会重新计算 
                value = runner() 
                dirty = false 
            } 
            // 依赖收集,收集运行访问该计算属性的 activeEffect 
            track(computed, "get" /* GET */, 'value') 
            return value 
        }, 
        set value(newValue) { 
            // 计算属性的 setter 
            setter(newValue) 
        } 
    } 
    return computed 
}

可以看到,computed 函数的流程主要做了三件事情:标准化参数,创建副作用函数和创建 computed 对象。我们来详细分析一下这几个步骤。

首先是标准化参数。computed 函数接受两种类型的参数,一个是 getter 函数,一个是拥有 getter 和 setter 函数的对象,通过判断参数的类型,我们初始化了函数内部定义的 getter 和 setter 函数。

接着是创建副作用函数 runner。computed 内部通过 effect 创建了一个副作用函数,它是对 getter 函数做的一层封装,另外我们这里要注意第二个参数,也就是 effect 函数的配置对象。其中 lazy 为 true 表示 effect 函数返回的 runner 并不会立即执行;computed 为 true 用于表示这是一个 computed effect,用于 trigger 阶段的优先级排序,我们稍后会分析;scheduler 表示它的调度运行的方式,我们也稍后分析。

最后是创建 computed 对象并返回,这个对象也拥有 getter 和 setter 函数。当 computed 对象被访问的时候会触发 getter,然后会判断是否 dirty,如果是就执行 runner,然后做依赖收集;当我们直接设置 computed 对象时会触发 setter,即执行 computed 函数内部定义的 setter 函数。