VUE-Computed缓存解析及使用注意点

383 阅读1分钟

前言

Vue中,我们基本都会使用到computed,用来作为计算属性,为了优化性能,computed会自带缓存,直到computed所依赖属性变化了,computed的值才会更改。

原理

function computed(getterOrOptions, debugOptions) {
    let getter;
    let setter;
    const onlyGetter = isFunction(getterOrOptions);
    if (onlyGetter) {
        getter = getterOrOptions;
        setter = () => {
                warn$2('Write operation failed: computed value is readonly');
            }
            ;
    }
    else {
        getter = getterOrOptions.get;
        setter = getterOrOptions.set;
    }
    const watcher = isServerRendering()
        ? null
        : new Watcher(currentInstance, getter, noop, { lazy: true });
    if (watcher && debugOptions) {
        watcher.onTrack = debugOptions.onTrack;
        watcher.onTrigger = debugOptions.onTrigger;
    }
    const ref = {
        // some libs rely on the presence effect for checking computed refs
        // from normal refs, but the implementation doesn't matter
        effect: watcher,
        get value() {
            if (watcher) {
                if (watcher.dirty) {
                    watcher.evaluate();
                }
                if (Dep.target) {
                    if (Dep.target.onTrack) {
                        Dep.target.onTrack({
                            effect: Dep.target,
                            target: ref,
                            type: "get" /* TrackOpTypes.GET */,
                            key: 'value'
                        });
                    }
                    watcher.depend();
                }
                return watcher.value;
            }
            else {
                return getter();
            }
        },
        set value(newVal) {
            setter(newVal);
        }
    };
    def(ref, RefFlag, true);
    def(ref, "__v_isReadonly" /* ReactiveFlags.IS_READONLY */, onlyGetter);
    return ref;
}

Vue会通过监听对象Watcher对computed中所涉及的依赖变量进行监听,依赖变量变化时,这个Watcher会被触发update(),computed返回的实际是一个代理对象,在访问这个computed变量时,或进入get(),首选第一步就是访问这个Watcher是否为dirty,也就是依赖变量有没有变化,如果没变化,就会返回上一次的值,有变化就会触发watcher的evaluate函数,重新触发我们的getter计算,并把dirty设为false

下面贴一下Watcher的evalute和update方法,可以更好的理解一下。

/**
     * Evaluate the value of the watcher.
     * This only gets called for lazy watchers.
     */
    evaluate() {
        this.value = this.get();
        this.dirty = false;
    }
    
/**
     * Subscriber interface.
     * Will be called when a dependency changes.
     */
    update() {
        /* istanbul ignore else */
        if (this.lazy) {
            this.dirty = true;
        }
        else if (this.sync) {
            this.run();
        }
        else {
            queueWatcher(this);
        }
    }

watcher的lazy属性,是上面定义computed的时候,就把watcher的lazy属性设为false了

const watcher = isServerRendering()
        ? null
        : new Watcher(currentInstance, getter, noop, { lazy: true });

注意点

父子组件传递值

既然computed是有缓存的,那么我们就得谨慎什么时候这个computed的值是最新的。下面看一个特殊用法

我们写表单的时候,如果将某一项单独写成一个子组件会更简洁 , 下面写个很粗糙的例子

父组件

<template>
   <div>
      <Input v-model="value"></Input>
   </div>
</template>
​
<script setup>
import Input from "./Input.vue";
import { ref } from "vue";
const value = ref(1);
</script>
<style scoped></style>

子组件

<template>
   <el-button type="primary" @click="handleUpdate">update</el-button>
</template>
​
<script setup>
import { computed } from "vue";
​
const props = defineProps({
   value: {type:Number},
});
​
const emit = defineEmits(["input"]);
​
const valueComputed = computed({
   get() {
      return props.value;
   },
   set(val) {
   // 这里用的是2.7版本,如果是vue3,应该使用 emit("update:value", val);
      emit("input", val);
   },
});
const handleUpdate = () => {
   // 一些逻辑
   valueComputed.value = 2;
   console.log(valueComputed.value)
   // 一些逻辑
   valueComputed.value = 3;
   console.log(valueComputed.value)
};
</script>
<style scoped lang="scss"></style>

第一次点击update的时候,大家可能会觉得会输出4,8,valueComputed的依赖已经变了,console.log的值应该是最新的,但其实并不是,两次console.log都是上一次值3

为什么呢,如果computed里面的依赖如果不是父组件的,而是本组件的响应式变量,那自然就是2,3,这里因为依赖的是父组件的变量,valueComputed赋值的时候( valueComputed.value = 2),触发input事件,父组件的value会被更新为4,然后触发依赖,更新子组件的值,但是更新子组件的值并不是同步的,而是放在queueWatcher队列中,通过nextTick去更新,所以执行完valueComputed.value = 2后,valueComputed.value所依赖的prop.value还没有被更新,此时valueComputed的Watcher中dirty仍未false,自然就是上一个值1了。

通常我们写大表单的时候,会把某些复杂子项单独写一个输入组件,如果通过computed来作中间件,控制父子组件的数据流,就得小心上面这种情况,避免在同步操作时出现set get同步。

避免Getter中有副作用

计算属性的 getter 应只做计算而没有任何其他的副作用,也就是说,不要改变其他状态、在 getter 中做异步请求或者更改 DOM! ,因为这样可能会使得你的computed失去响应式计算且计算值有误

/**
     * Evaluate the getter, and re-collect dependencies.
     */
    get() {
        pushTarget(this);
        let value;
        const vm = this.vm;
        try {
            value = this.getter.call(vm, vm);
        }
        catch (e) {
            if (this.user) {
                handleError(e, vm, `getter for watcher "${this.expression}"`);
            }
            else {
                throw e;
            }
        }
        finally {
            // "touch" every property so they are all tracked as
            // dependencies for deep watching
            if (this.deep) {
                traverse(value);
            }
            popTarget();
            this.cleanupDeps();
        }
        return value;
    }

从上面的Watcher对象的get方法可知道,如果你的getter 存在副作用,使用了一些异步请求,value获取不了值,计算值会为undefined,并且无法收集异步后的依赖

如下面的例子

const dep1=ref()
const dep2=ref()
const todo=computed(async()=>{
    const {data}=await getData(dep1.value)
    total =total+dep2.value
    return total
})

这个todo计算值会失去dep2的响应式依赖收集,并且计算值会始终为undefined

以上例子可借助watch写成:

const dep1=ref()
const dep2=ref()
const dep3=ref()
watch(dep1,async()=>{
    const {data}=await getData(dep1.value)
    dep3.value=data
})
const todo=computed(async()=>{
    return dep2.value+dep3.value
})