watch
一、认识
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
: 在侦听器创建时立即触发回调。第一次调用时旧值是undefined
。watch()
默认是懒侦听的,即仅在侦听源发生变化时才执行回调函数。 -
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 侦听多个数据源
侦听多个数据源时, newValue
和 oldValue
与 source
对应, 也是两个数组, 分别为: 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
来观察 数据源(可以是 ref
、reactive
对象、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
来观察 数据源(可以是 ref
、reactive
对象、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
响应式系统的重要组成部分。
因此, Watch
和 Computed
依赖收集都是基于 ReactiveEffect
来实现的, 也都传入了调度器进行定制逻辑。但是, Computed
的 ReactiveEffect
包装的是传入的计算函数; Watch
中的 ReactiveEffect
包装的是观察 数据源(可以是 ref
、reactive
对象、getter
函数或它们的组合)的函数。