前言 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 = { 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" , key : 'value' }); } watcher.depend (); } return watcher.value ; } else { return getter (); } }, set value (newVal ) { setter (newVal); } }; def (ref, RefFlag , true ); def (ref, "__v_isReadonly" , onlyGetter); return ref; }
Vue会通过监听对象Watcher对computed中所涉及的依赖变量进行监听,依赖变量变化时,这个Watcher会被触发update(),computed返回的实际是一个代理对象,在访问这个computed变量时,或进入get(),首选第一步就是访问这个Watcher是否为dirty,也就是依赖变量有没有变化,如果没变化,就会返回上一次的值,有变化就会触发watcher的evaluate函数,重新触发我们的getter计算,并把dirty设为false
下面贴一下Watcher的evalute和update方法,可以更好的理解一下。
evaluate ( ) { this .value = this .get (); this .dirty = false ; } update ( ) { 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失去响应式计算且计算值有误
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 { 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 })