Vue3中computed、watch、watchEffect详解

1,190 阅读5分钟

思维导图

Vue监听器.jpg

使用场景

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首次执行的区别

截屏2024-06-23 21.40.21.png

如上图,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;
}
参数含义
flushpre默认,表示立即更新Dom
flushpostwatch的值变化让DOM更新之后,再去执行回调
flushsync同步修改,相当于取消了“细节一”的合并处理
immediatefalse默认,watch不会立即执行
immediatetrue设置了true之后,watch也会像watchEffect一样初始化执行一次
deepfalse默认,如果监听的是对象,那么对象的属性变化时不会触发
deeptrue会遍历对象的属性,如果有变化,也会触发更新回调
oncefalse默认,一直监听变化
oncetrue只监听一次,触发后就自动取消了

细节五: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…