跳到主要内容

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 watch 实现原理

Vue 3 中, Watch 是基于响应式系统构建的一个高层 API,用于观察响应式数据的变化并在数据变化时触发回调。Watch 具体工作流如下: 1. 响应式依赖追踪, Watch 的本质是创建一个 ReactiveEffect 来观察 数据源(可以是 refreactive 对象、getter 函数或它们的组合)。当数据源中依赖的数据发生变化时, ReactiveEffect 会触发调度器,进而执行 watch 回调。2. 懒执行与调度, Watch 通过传入自定义的调度器(scheduler), 可以控制何时执行回调函数, 默认情况下, watch 会异步调度更新(比如在微任务队列中), 也支持同步(flush: 'sync')或 post-render(flush: 'post')等不同的调度时机。3. 清理与失效, 为了处理异步回调中可能存在的竞争问题,watch 提供了 onInvalidate 回调,允许用户注册清理函数, 如果在异步任务期间依赖发生变化,则会调用该清理函数,以防止过时的回调继续执行。

一、Watch 底层结构

function traverse(value, seen = new Set()) {
// 如果 value 不是对象或为空,则直接返回
if (typeof value !== 'object' || value === null) {
return value;
}

// 如果已经访问过这个对象,则直接返回,防止循环引用
if (seen.has(value)) {
return value;
}
seen.add(value);

// 如果是数组,则遍历每个元素
if (Array.isArray(value)) {
for (let i = 0; i < value.length; i++) {
traverse(value[i], seen);
}
}
// 如果是 Map,则遍历每个键和值
else if (value instanceof Map) {
value.forEach((v, k) => {
traverse(k, seen);
traverse(v, seen);
});
}
// 如果是 Set,则遍历每个成员
else if (value instanceof Set) {
value.forEach(v => {
traverse(v, seen);
});
}
// 如果是普通对象,则遍历所有属性
else {
for (const key in value) {
traverse(value[key], seen);
}
}

return value;
}
function watch(source, cb, options = {}) {
// 1. 生成 getter
let getter;
if (typeof source === 'function') {
getter = source;
} else {
// 对非函数类型,使用 traverse 进行深度遍历(若 deep 选项为 true)
getter = () => traverse(source);
}

let oldValue, newValue;
let cleanup;

// onInvalidate 回调,用于注册清理函数
function onInvalidate(fn) {
cleanup = fn;
}

// 2. 定义 job 函数,当依赖更新时执行
const job = () => {
// 如果存在清理函数,先调用,清除之前的异步任务等
if (cleanup) cleanup();
newValue = effect.run();
cb(newValue, oldValue, onInvalidate);
oldValue = newValue;
};

// 3. 创建 ReactiveEffect,传入调度器选项
const effect = new ReactiveEffect(getter, () => {
if (options.flush === 'sync') {
job();
} else {
// 异步调度,例如微任务队列中调度
Promise.resolve().then(job);
}
});

// 4. 立即执行或懒执行
if (options.immediate) {
job();
} else {
oldValue = effect.run();
}

// 5. 返回一个停止 watch 的函数
return () => effect.stop();
}

二、源的处理与 getter 的生成: watch 的第一个参数(source)可以是多种类型, 如果是函数, 直接作为 getter 使用; 如果是响应式对象、ref、数组, 内部会使用一个遍历函数(traverse)深度遍历,确保依赖全部被收集。根据 source 的类型,watch 会生成一个统一的 getter 函数,用来返回观察值。对于非函数类型,还会进行深度遍历(如果设置了 deep 选项)。

三、创建 ReactiveEffect: watch 利用 ReactiveEffect 包装生成的 getter,设置一个调度器(scheduler),这个调度器函数会在响应式数据更新时触发。scheduler 不会直接调用 getter,而是安排一个 job 函数。这个 job 函数: 1. 会重新执行 effect, 拿到最新的值; 2. 触发回调函数, 传入新旧值;3. 在回调执行前, 会先调用 onInvalidate 清理函数(如果注册了)。

4.2 computed vs watch

Watch 是基于响应式系统构建的一个高层 API,用于观察响应式数据的变化并在数据变化时触发回调。Watch 具体工作流如下: 1. 响应式依赖追踪, Watch 的本质是创建一个 ReactiveEffect 来观察 数据源(可以是 refreactive 对象、getter 函数或它们的组合)。当数据源中依赖的数据发生变化时, ReactiveEffect 会触发调度器,进而执行 watch 回调。2. 懒执行与调度, Watch 通过传入自定义的调度器(scheduler), 可以控制何时执行回调函数, 默认情况下, watch 会异步调度更新(比如在微任务队列中), 也支持同步(flush: 'sync')或 post-render(flush: 'post')等不同的调度时机。3. 清理与失效, 为了处理异步回调中可能存在的竞争问题,watch 提供了 onInvalidate 回调,允许用户注册清理函数, 如果在异步任务期间依赖发生变化,则会调用该清理函数,以防止过时的回调继续执行。

Computed 是一个计算属性, 根据传入的 getter 函数, 计算得到一个可读或者可读可写的响应式数据。Computed 具有惰性计算、缓存更新等特点。在 Vue 3 中, Computed 的实现是建立在响应式系统和 ReactiveEffect 之上的一种缓存计算机制, 主要依赖于 ReactiveEffect 来包装计算函数, 并利用调度器和 dirty 标记以及 _value 实现惰性计算与缓存更新。这种设计既保证了 Computed 只在必要时重新计算,又能高效地追踪依赖,实现高性能的响应式数据更新,是 Vue 3 响应式系统的重要组成部分。

因此, WatchComputed 依赖收集都是基于 ReactiveEffect 来实现的, 也都传入了调度器进行定制逻辑。但是, ComputedReactiveEffect 包装的是传入的计算函数; Watch 中的 ReactiveEffect 包装的是观察 数据源(可以是 refreactive 对象、getter 函数或它们的组合)的函数。