认识
一、认识
在 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
清理函数(如果注册了)。
二、变量细节
2.1 getter
2.2 cleanup
watch
提供清除过期副作用的能力, 这个能力的实现通过 cleanup
。基本逻辑如下:
function watch(source,cb,options = {}){
let cleanup;
function onInvalidate(fn){
cleanup = fn;
}
const job = ()=>{
newValue = effectFn();
if(cleanup){
cleanup();
}
cb(newValue,oldValue,onInvalidate);
oldValue = newValue;
}
}
如上所示, 通过 cleanup
存储用户注册的过期回调, 每当 watch
回调函数执行之前, 会优先执行用户通过 onInvalidate
注册的过期回调。 这样, 用户就有机会在过期回调中将上一次的副作用标记为 过期, 从而以 忽略请求数据的方式 解决竞态问题。调用方式如下所示:
function request() {
return new Promise(resolve => {
setTimeout(() => {
resolve();
}, 1000);
});
}
watch(
() => obj.c,
async (newValue, oldValue, onInvalidate) => {
let expired = false;
onInvalidate(() => {
expired = true;
});
await request();
if (!expired) {
console.log(newValue, oldValue);
}
}
);
obj.c++;
setTimeout(() => {
obj.c++;
}, 200);
三、函数细节
3.1 job
job
为 scheduler
函数中抽离的主要逻辑, 基本实现如下:
const job = ()=>{
newValue = effectFn();
cb(newValue,oldValue);
oldValue = newValue;
}
3.2 scheduler
scheduler
为 effect
的调度器, 可以控制副作用函数的执行顺序与执行次数。如果 effect
存在 scheduler
配置项, 当响应式数据发生变化时, 会触发 scheduler
调度函数执行, 而不是直接触发副作用函数。机制如下:
effect(
()=>{ console.log(obj.a); },
{
scheduler(fn){
fn();
}
}
);
四、问题
4.1 computed vs watch
watch
用于监测响应式数据, 当数据发生变化时, 通知并执行相应的回调函数。watch
传入的第一个参数为要监测的响应式数据, watch
中创建一个 effect
副作用, 第一个参数为封装好副作用函数, 这个副作用函数主要是读取传入的响应式数据, 触发响应式数据的副作用收集机制, 传入 Scheduler
配置项, 在 effect
中, 如果有 Scheduler
配置项, 那么执行 Scheduler
调度函数执行, 而不是直接触发副作用函数执行。在 Scheduler
调度函数中, 执行传入的 watch
回调。watch
有三个参数, 侦听器的源, 发生变化的回调, 和一个配置项, 可以配置 immediate
立即监听, 可以配置 deep
进行深度监听。 在变化的回调中, 同样提供了三个参数, 最新值, 旧值, 以及一个用于注册副作用清理的回调函数 cleanup
。 cleanup
可以用于解决一些竟态问题, 在响应式数据发生变化后, 调用 cleanup
, 并传入想要取消的逻辑代码, 优先取消之前的副作用影响后,再开始新的副作用。
computed
是一个计算属性, 根据传入的 getter
函数, 计算得到一个可读或者可读可写的响应式数据。 computed
本质上是将传入的 getter
作为一个副作用函数, 创建一个带有 lazy
和 scheduler
的 effect
, 返回一个具有访问器属性value
和 设置器属性value
的数据。在读取 computed
数据时, 访问器进行拦截, 在内部变量 dirty
为 true
的情况下, 执行通过 effect
返回的 effectFn
函数, 这个 effectFn
内部会执行 computed
传入的 getter
进行计算。因此, computed
同 ref
一样, 是一个新的状态数据, 并具有延时计算和缓存计算结果的功能。