思维导图
使用场景
computed:声明一个依赖于其他响应式数据属性的属性,并且这个属性的值会根据其依赖的数据的变化而自动更新。
watch、watchEffect:用于响应式数据发生变化时执行一个函数,在使用细节上有所区别,后面会详细说明。
基本用法Demo
<a-input type="number" prefix="平时分:" v-model:value="score1" placeholder="请输入平时分" />
<a-input type="number" prefix="期末分:" v-model:value="score2" placeholder="请输入期末分" />
<a-input type="number" prefix="附加分:" v-model:value="score3" placeholder="请输入附加分" />
<a-button type="primary" @click="doAdd">全体加一</a-button>
<div>computed计算总分:{{ total1 }}</div>
<div>watch(逐个监听)计算总分:{{ total2 }}</div>
<div>watch(监听全部)计算总分:{{ total3 }}</div>
<div>watchEffect(自动监听依赖)计算总分:{{ total4 }}</div>
const score1 = ref(80);
const score2 = ref(80);
const score3 = ref(5);
function calcTotal(a, b, c) {
return (Number(a * 0.2) || 0) + (Number(b * 0.8) || 0) + (Number(c) || 0);
}
computed用法
总分由三个输入框的值计算得到,可以在computed里传入一个函数,函数里return一个值,函数中使用了响应式数据。这样响应式数据发生变化的时候,就会触发computed,自动计算total1。
// 用computed,可以得到计算后的值
const total1 = computed(() => {
return calcTotal(score1.value, score2.value, score3.value);
});
watch用法(一)
我们的总分是由三个输入的分数决定,因此我们可以监听每一个输入框的值,发生变化时,触发一个函数,去计算总分。这种思路就和JQuery一样,记录数据变化的过程,然后处理变化。
const total2 = ref(0);
// 按照watch的思路,就需要逐个监听
watch(score1, val => {
total2.value = calcTotal(val, score2.value, score3.value);
});
watch(score2, val => {
total2.value = calcTotal(score1.value, val, score3.value);
});
watch(score3, val => {
total2.value = calcTotal(score1.value, score2.value, val);
});
watch用法(二)
逐个监听意味着我们需要写很多个watch,实际上我们可以用一个watch处理,只需要watch的第一个参数传入一个函数,函数返回三个响应式数据,代码如下:
// watch的另一种用法,将监听的数据组合在一起
const total3 = ref(0);
watch(
() => [score1.value, score2.value, score3.value],
function (val) {
total3.value = calcTotal(val[0], val[1], val[2]);
}
);
watchEffect用法
watchEffect 先执行,并且自动监听了依赖的数据。watchEffect用起来比较像computed,区别在于computed会返回一个值,而watchEffect是执行一个函数。
const total4 = ref(0);
watchEffect(() => {
total4.value = calcTotal(score1.value, score2.value, score3.value);
});
上面的例子,都是前端计算分数,因此computed是更好的选择;如果是调用接口计算分数,则watch/watchEffect更合适。
用法细节Demo
细节一:合并变化
如Demo所示,点击按钮同时改变三个变量值,computed、watch、watchEffect并不会执行三次,而是将变化合并之后,只执行一次。
// 全体+1,合并的监听操作,不会重复执行
function doAdd() {
score1.value = Number(score1.value) + 1;
score2.value = Number(score2.value) + 1;
score3.value = Number(score3.value) + 1;
}
细节二:watch和watchEffect首次执行的区别
如上图,watch并没有一开始就执行,而watchEffect是先执行了,然后监听了数据变化。
细节三:关闭监听的方法
export type WatchStopHandle = () => void;
export declare function watchEffect(effect: WatchEffect, options?: WatchOptionsBase): WatchStopHandle;
export declare function watch<T, Immediate extends Readonly<boolean> = false>(source: WatchSource<T>, cb: WatchCallback<T, Immediate extends true ? T | undefined : T>, options?: WatchOptions<Immediate>): WatchStopHandle;
export declare function watch<T extends MultiWatchSources, Immediate extends Readonly<boolean> = false>(sources: [...T], cb: WatchCallback<MapSources<T, false>, MapSources<T, Immediate>>, options?: WatchOptions<Immediate>): WatchStopHandle;
export declare function watch<T extends Readonly<MultiWatchSources>, Immediate extends Readonly<boolean> = false>(source: T, cb: WatchCallback<MapSources<T, false>, MapSources<T, Immediate>>, options?: WatchOptions<Immediate>): WatchStopHandle;
export declare function watch<T extends object, Immediate extends Readonly<boolean> = false>(source: T, cb: WatchCallback<T, Immediate extends true ? T | undefined : T>, options?: WatchOptions<Immediate>): WatchStopHandle;
从watch、watchEffect的定义中,我们可以看到,watch和watchEffect的返回值是WatchStopHandle函数,因此我们只需要执行一下,即可取消监听了。
const cancelWatchEffect = watchEffect(() => {
total4.value = calcTotal(score1.value, score2.value, score3.value);
});
cancelWatchEffect() // 取消监听
细节四:watchEffect的第二个参数 & watch的第三个参数
细节三中,我们看到了watchEffect和watch都有一个options参数,该参数的定义如下:
export interface WatchOptionsBase extends DebuggerOptions {
flush?: 'pre' | 'post' | 'sync';
}
export interface WatchOptions<Immediate = boolean> extends WatchOptionsBase {
immediate?: Immediate;
deep?: boolean;
once?: boolean;
}
| 参数 | 值 | 含义 |
|---|---|---|
| flush | pre | 默认,表示立即更新Dom |
| flush | post | watch的值变化让DOM更新之后,再去执行回调 |
| flush | sync | 同步修改,相当于取消了“细节一”的合并处理 |
| immediate | false | 默认,watch不会立即执行 |
| immediate | true | 设置了true之后,watch也会像watchEffect一样初始化执行一次 |
| deep | false | 默认,如果监听的是对象,那么对象的属性变化时不会触发 |
| deep | true | 会遍历对象的属性,如果有变化,也会触发更新回调 |
| once | false | 默认,一直监听变化 |
| once | true | 只监听一次,触发后就自动取消了 |
细节五:watchEffect第一个参数是一个函数,这个函数的参数也是一个函数
watchEffect第一个参数是个函数(称作函数A),这个函数的参数也可以是个函数(函数B)。函数B调用的时机是下一次执行watchEffect、或者被取消、被卸载.
const total4 = ref(0);
watchEffect(clean => {
total4.value = calcTotal(score1.value, score2.value, score3.value);
clean(() => {
console.log("执行了clean");
});
});
看看应用的业务场景(代码来自vben)
watchEffect使用场景:父组件的props作为子组件输入框的初始值
src/components/Cropper
watchEffect(() => {
sourceValue.value = props.value || '';
});
function handleUploadSuccess({ source }) {
sourceValue.value = source;
emit('change', source);
createMessage.success(t('component.cropper.uploadSuccess'));
}
我们不会直接修改父组件传过来的props,一般都是把这个值作为子组件响应式数据的初始值。
watchEffect使用场景:组件库冲突属性的监控
src/components/Table/src/BasicTable.vue
watchEffect(() => {
unref(isFixedHeightPage) &&
props.canResize &&
warn(
"'canResize' of BasicTable may not work in PageWrapper with 'fixedHeight' (especially in hot updates)",
);
});
flush:post的使用场景,页面变化之后重新计算高度
src/components/Page/src/PageWrapper.vue
如下面的代码是pageWrapper组件,getShowFooter.value控制了页面上的底部区域显示与否,redoHeight是重新计算了内容区域的高度。显然,我们需要在确定footer展示或隐藏了之后,才能计算内容区域的高度。
watch(
() => [getShowFooter.value],
() => {
redoHeight();
},
{
flush: 'post',
immediate: true,
},
);
总结
Demo地址:github.com/beat-the-bu…