跳到主要内容

watch

2024年03月18日
柏拉文
越努力,越幸运

一、认识


watch 侦听一个或多个响应式数据源,并在数据源变化时调用所给的回调函数。与 watchEffect() 相比,watch() 使我们可以:

  • 懒执行副作用

  • 更加明确是应该由哪个状态触发侦听器重新执行

  • 可以访问所侦听状态的前一个值和当前值。

二、语法


watch(
source: Ref | reactive | object | getter | (Ref | reactive | object | getter) []
(newValue,oldValue,onCleanup)=>{},
options?: {
immediate?: boolean,
deep?: boolean,
flush?: 'pre' | 'post' | 'sync',
onTrack?: (event)=> void
onTrigger?: (event)=> void
})
  • source: 侦听器的源。这个来源可以是以下几种:

    • 一个函数,返回一个值

    • 一个 ref

    • 一个响应式对象

    • ...或是由以上类型的值组成的数组

  • callback: 在发生变化时要调用的回调函数。这个回调函数接受三个参数: 新值、旧值,以及一个用于注册副作用清理的回调函数。该回调函数会在副作用下一次重新执行前调用,可以用来清除无效的副作用,例如等待中的异步请求。当侦听多个来源时,回调函数接受两个数组,分别对应来源数组中的新值和旧值。

  • options: 可选, 是一个对象,支持以下这些选项:

    • immediate: 在侦听器创建时立即触发回调。第一次调用时旧值是 undefinedwatch() 默认是懒侦听的,即仅在侦听源发生变化时才执行回调函数。

    • deep: 如果源是对象,强制深度遍历,以便在深层级变更时触发回调

    • flush: post | sync。调整回调函数的刷新时机

    • onTrack / onTrigger: 调试侦听器的依赖

三、用法


3.1 侦听 Ref

const a = ref(0);

watch(a, (newValue, oldValue) => {
console.log(newValue, oldValue);
});

a.value += 1;

3.2 侦听 Reactive

侦听整个 Reactive

import { reactive, watch } from 'vue';

const b = reactive({ c: 1 });

watch(b, (newValue, oldValue) => {
console.log(newValue, oldValue);
});

b.c++;

侦听 Reactive 具体属性

import { reactive, watch } from 'vue';

const b = reactive({ c: 1 });

watch(b.c, (newValue, oldValue) => {
console.log(newValue, oldValue);
});

b.c++;

3.3 侦听 getter

当使用 getter 作为数据源时, 回调函数只在此函数的返回值发生变化时才会触发。

import { ref, reactive, watch } from 'vue';

const a = ref(0);

const b = reactive({ c: 1 });

watch(
() => a.value + b.c,
(newValue, oldValue) => {
console.log(newValue, oldValue);
}
);

a.value += 1;

3.4 侦听多个数据源

侦听多个数据源时, newValueoldValuesource 对应, 也是两个数组, 分别为: newValue = [数据源新值]oldValue = [数据源旧值]

import { ref, reactive, watch } from 'vue';

const a = ref(0);

const b = reactive({ c: 1 });

watch([a, b], (newValue, oldValue) => {
console.log(newValue, oldValue);
});

a.value += 1;

3.5 停止侦听器

import { ref, watch } from 'vue';

const a = ref(0);

const stop = watch(a, (newValue, oldValue) => {
console.log(newValue, oldValue);
});

a.value += 1; // 触发 watch 回调

setTimeout(() => {
stop();
}, 2000);

a.value += 1; // 不会触发 watch 回调

3.6 开启深层侦听

3.7 清除过期副作用

通过标记的方式清除过期副作用, 解决竞态问题: 只处理最后一次的请求响应数据, 进而解决竞态问题

import { ref, watch } from 'vue';

const a = ref(0);

function request() {
return new Promise(resolve => {
setTimeout(() => {
resolve(true);
}, 2000);
});
}

watch(a, async (newValue, oldValue, onCleanup) => {
let expired = false;

onCleanup(() => {
expired = true;
});

await request();

if (!expired) {
console.log(newValue, oldValue);
}
});

a.value += 1;

setTimeout(() => {
a.value += 1;
}, 1000);

通过取消请求的方式清除过期副作用, 解决竞态问题: 通过取消之前请求, 进而解决竞态问题

import { ref, watch } from 'vue';

const a = ref(0);

function request() {
return new Promise(resolve => {
setTimeout(() => {
resolve(true);
}, 2000);
});
}

watch(a, async (newValue, oldValue, onCleanup) => {
const { response, cancel } = doAsyncWork(newValue)
onCleanup(() => {
cancel(); // 取消之前请求
});

await request(); // 始终只发送最新请求
});

a.value += 1;

setTimeout(() => {
a.value += 1;
}, 1000);

3.8 收集、管理副作用

import { ref, watch, watchEffect } from 'vue';

let disposables = [];
const count = ref(0);

const watchCount = watch(()=> count.value, ()=> console.log("watch count", count.value));
disposables.push(watchCount);

const watchEffectCout = watchEffect(()=> console.log('watchEffect count', count.value));
disposables.push(watchEffectCout);

setTimeout(()=>{
count.value++;
},2000);

setTimeout(()=>{
disposables.forEach(dispose=> dispose());
disposables = []
},4000);

setTimeout(()=>{
count.value++;
}, 60000);

3.9 指定回调函数立即执行

3.10 指定回调函数执行时机

四、问题


4.1 computed vs watch

watch 用于监测响应式数据, 当数据发生变化时, 通知并执行相应的回调函数。watch 传入的第一个参数为要监测的响应式数据, watch 中创建一个 effect 副作用, 第一个参数为封装好副作用函数, 这个副作用函数主要是读取传入的响应式数据, 触发响应式数据的副作用收集机制, 传入 Scheduler 配置项, 在 effect 中, 如果有 Scheduler 配置项, 那么执行 Scheduler 调度函数执行, 而不是直接触发副作用函数执行。在 Scheduler 调度函数中, 执行传入的 watch 回调。watch 有三个参数, 侦听器的源, 发生变化的回调, 和一个配置项, 可以配置 immediate 立即监听, 可以配置 deep 进行深度监听。 在变化的回调中, 同样提供了三个参数, 最新值, 旧值, 以及一个用于注册副作用清理的回调函数 cleanupcleanup 可以用于解决一些竟态问题, 在响应式数据发生变化后, 调用 cleanup, 并传入想要取消的逻辑代码, 优先取消之前的副作用影响后,再开始新的副作用。

computed 是一个计算属性, 根据传入的 getter 函数, 计算得到一个可读或者可读可写的响应式数据。 computed 本质上是将传入的 getter 作为一个副作用函数, 创建一个带有 lazyschedulereffect, 返回一个具有访问器属性value设置器属性value 的数据。在读取 computed 数据时, 访问器进行拦截, 在内部变量 dirtytrue 的情况下, 执行通过 effect 返回的 effectFn 函数, 这个 effectFn 内部会执行 computed 传入的 getter 进行计算。因此, computedref 一样, 是一个新的状态数据, 并具有延时计算和缓存计算结果的功能。