前言

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
})