跳到主要内容

认识

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

一、认识


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 清理函数(如果注册了)。

二、变量细节


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

jobscheduler 函数中抽离的主要逻辑, 基本实现如下:

const job = ()=>{
newValue = effectFn();
cb(newValue,oldValue);
oldValue = newValue;
}

3.2 scheduler

schedulereffect 的调度器, 可以控制副作用函数的执行顺序与执行次数。如果 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 进行深度监听。 在变化的回调中, 同样提供了三个参数, 最新值, 旧值, 以及一个用于注册副作用清理的回调函数 cleanupcleanup 可以用于解决一些竟态问题, 在响应式数据发生变化后, 调用 cleanup, 并传入想要取消的逻辑代码, 优先取消之前的副作用影响后,再开始新的副作用。

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