在前几篇文章中,我们逐步构建起了 Vue3 响应式系统的完整图景:从
reactive/ref创建响应式数据,到track收集依赖,trigger触发更新,再到computed实现懒计算。今天,我们将探索响应式系统的最后一个核心 API——watch和watchEffect,看看它们如何监听数据变化并执行回调。
前言:为什么需要 watch?
在 Vue3 中,我们有多种方式响应数据变化:
- computed:派生数据
const double = computed(() => count.value * 2);
- 渲染函数:自动响应
render();
- effect:底层副作用
effect(() => {
console.log(count.value);
});
- watch/watchEffect:监听变化执行回调
watchEffect(() => {
console.log(count.value); // 立即执行,自动收集依赖
});
watch(count, (newVal, oldVal) => {
console.log(`count 从 ${oldVal} 变为 ${newVal}`); // 懒执行,需要指定源
});
| API | 执行时机 | 适用场景 | 特点 |
|---|---|---|---|
| computed | 依赖变化时 | 派生数据 | 缓存结果,惰性计算 |
| render | 数据变化时 | 视图更新 | 自动触发,不可控 |
| effect | 立即执行 | 底层副作用 | 最基础,需手动管理 |
| watchEffect | 立即执行+自动追踪 | 需要自动收集依赖 | 简洁但不够灵活 |
| watch | 懒执行+指定源 | 需要精确控制 | 灵活,可获取新旧值 |
其中,watch 和 watchEffect 的核心区别在于:
watchEffect:立即执行,会自动收集所有依赖,不需要指定要监听的数据watch:懒执行,需要明确指定监听的数据源,可以获取新旧值
watchEffect 的实现
核心原理
watchEffect 本质上是 effect 的一个封装,但它有几个特点:
- 立即执行一次(收集依赖)
- 自动跟踪所有响应式依赖
- 依赖变化时重新执行
watchEffect 的简化版本
/**
* watchEffect 最小实现
* @param {Function} effect - 要执行的副作用函数
* @returns {Function} stop - 停止监听的函数
*/
function watchEffect(effect) {
// 创建一个 ReactiveEffect,传入调度器
const _effect = new ReactiveEffect(effect, () => {
// 调度器:当依赖变化时,触发 effect 重新运行
_effect.run();
});
// 立即执行一次,收集依赖
_effect.run();
// 返回停止监听的函数
return () => {
_effect.stop();
};
}
完整实现
/**
* 完整的 watchEffect 实现
* 包含:调度控制、停止监听、清理机制、错误处理、调试钩子
*/
function watchEffect(effect, options = {}) {
// 1. 参数标准化与默认值
const {
flush = 'pre', // 执行时机:pre/post/sync
onTrack, // 调试钩子:依赖追踪时
onTrigger, // 调试钩子:触发更新时
onStop // 停止回调
} = options;
// 2. 清理函数管理
let cleanup;
function onInvalidate(fn) {
cleanup = fn;
}
// 3. 包装用户 effect,注入清理能力
const wrappedEffect = () => {
// 执行前清理
if (cleanup) {
cleanup();
}
// 执行用户 effect,传入清理注册函数
effect(onInvalidate);
};
// 4. 创建调度器
const scheduler = createScheduler(flush, () => {
_effect.run();
});
// 5. 创建 ReactiveEffect
const _effect = new ReactiveEffect(wrappedEffect, scheduler);
// 6. 附加调试钩子
if (onTrack) _effect.onTrack = onTrack;
if (onTrigger) _effect.onTrigger = onTrigger;
// 7. 立即执行收集依赖
_effect.run();
// 8. 返回增强的停止函数
const stop = () => {
_effect.stop();
if (cleanup) {
cleanup();
}
onStop?.();
};
return stop;
}
/**
* 创建调度器
* @param {string} flush - 执行时机
* @param {Function} job - 要执行的任务
*/
function createScheduler(flush, job) {
// 同步执行
if (flush === 'sync') {
return job;
}
// 异步队列
const queue = new Set();
let isFlushing = false;
const queueJob = () => {
queue.add(job);
if (!isFlushing) {
isFlushing = true;
if (flush === 'post') {
// 组件更新后执行
Promise.resolve().then(flushJobs);
} else {
// 'pre' 或默认:组件更新前执行
queueMicrotask(flushJobs);
}
}
};
function flushJobs() {
try {
queue.forEach(j => j());
} finally {
queue.clear();
isFlushing = false;
}
}
return queueJob;
}
源码原理分析
Vue3 源码中 watchEffect 的关键设计:
- 为什么使用 ReactiveEffect 而不是直接调用 effect?
- ReactiveEffect 提供了完整的生命周期管理
- 支持暂停、恢复、停止等操作
- 内置调度器机制,实现批量更新
- 依赖收集的时机:
- 首次执行:收集所有访问的响应式属性
- 后续执行:每次运行都会重新收集(解决条件分支问题)
- 调度器的设计哲学:
- 异步批量:避免重复执行,提升性能
- 优先级控制:pre(组件前)/post(组件后)/sync(同步)
- 微任务队列:利用事件循环机制
watch 的实现
核心原理
watch 比 watchEffect 更复杂,需要处理:
- 多种类型的 source(ref、reactive、数组、函数)
- 获取新旧值
- 懒执行(默认不立即执行)
- 深度监听
- 清理机制
处理多种 source 类型
/**
* 标准化 watch 的 source 参数
* 支持的类型:
* - ref
* - reactive
* - 数组
* - 函数
*/
function normalizeSource(source) {
if (isRef(source)) {
// ref: 直接读取 .value
return () => source.value;
} else if (isReactive(source)) {
// reactive: 深度监听,需要递归遍历
return () => traverse(source);
} else if (Array.isArray(source)) {
// 数组: 每个元素单独处理
return () => source.map(s => normalizeSource(s)());
} else if (isFunction(source)) {
// 函数: 直接使用,但需要处理响应式依赖
return source;
} else {
// 无效 source
return () => {};
}
}
/**
* 递归遍历对象,收集所有属性作为依赖
* 用于实现 deep: true
*/
function traverse(value, seen = new Set()) {
if (!isObject(value) || seen.has(value)) {
return value;
}
seen.add(value);
if (Array.isArray(value)) {
value.forEach(item => traverse(item, seen));
} else if (value instanceof Map) {
value.forEach((v, k) => {
traverse(v, seen);
traverse(k, seen);
});
} else if (value instanceof Set) {
value.forEach(v => traverse(v, seen));
} else {
Object.keys(value).forEach(key => traverse(value[key], seen));
}
return value;
}
新旧值的获取
/**
* watch 的核心实现
* @param {any} source - 监听的数据源
* @param {Function} cb - 回调函数
* @param {Object} options - 配置选项
*/
function watch(source, cb, options = {}) {
let getter;
// 1. 标准化 source 为 getter 函数
if (isRef(source)) {
getter = () => source.value;
} else if (isReactive(source)) {
getter = () => source;
// reactive 默认深度监听
options.deep = options.deep ?? true;
} else if (Array.isArray(source)) {
getter = () => source.map(s => normalizeSource(s)());
} else if (isFunction(source)) {
if (cb) {
// watch 形式:函数作为数据源
getter = source;
} else {
// watchEffect 形式:函数就是 effect
return watchEffect(source, options);
}
}
// 2. 处理 deep 选项
if (options.deep) {
const baseGetter = getter;
getter = () => traverse(baseGetter());
}
// 3. 存储旧值
let oldValue;
// 4. 清理函数
let cleanup;
function onInvalidate(fn) {
cleanup = fn;
}
// 5. 创建调度器
const scheduler = () => {
if (cleanup) {
cleanup();
}
const newValue = getter();
// 调用回调,传入新旧值
cb(newValue, oldValue, onInvalidate);
// 更新旧值
oldValue = newValue;
};
// 6. 创建 effect
const _effect = new ReactiveEffect(getter, scheduler);
// 7. 立即执行一次,获取初始值
if (options.immediate) {
// immediate 模式:立即执行回调
scheduler();
} else {
// 普通模式:只获取旧值,不执行回调
oldValue = _effect.run();
}
// 8. 返回停止函数
return () => {
_effect.stop();
};
}
完整实现
function watch(source, cb, options = {}) {
// 如果没有提供回调,当作 watchEffect 处理
if (!isFunction(cb)) {
return watchEffect(source, options);
}
let getter;
let isMultiSource = false;
// 处理多种 source 类型
if (isRef(source)) {
getter = () => source.value;
} else if (isReactive(source)) {
getter = () => source;
options.deep = options.deep ?? true;
} else if (isArray(source)) {
isMultiSource = true;
getter = () => source.map(s => {
if (isRef(s)) return s.value;
if (isReactive(s)) return traverse(s);
if (isFunction(s)) return s();
return s;
});
} else if (isFunction(source)) {
getter = source;
} else {
getter = () => {};
}
// 处理 deep 监听
if (options.deep) {
const baseGetter = getter;
getter = () => traverse(baseGetter());
}
// 存储旧值
let oldValue;
// 清理函数
let cleanup;
function onInvalidate(fn) {
cleanup = fn;
}
// 调度器
const scheduler = () => {
if (cleanup) {
cleanup();
}
const newValue = getter();
// 比较新值和旧值(简化处理,未实现值比较)
if (newValue !== oldValue) {
cb(newValue, oldValue, onInvalidate);
}
oldValue = newValue;
};
// 创建 effect
const _effect = new ReactiveEffect(getter, scheduler);
// 处理 flush 选项
if (options.flush === 'sync') {
// 同步执行,使用原调度器
_effect.scheduler = scheduler;
} else {
// 异步执行(post/pre),包装调度器
const originalScheduler = scheduler;
_effect.scheduler = () => {
if (options.flush === 'post') {
// 组件更新后执行
Promise.resolve().then(originalScheduler);
} else {
// 'pre' 或默认,简化处理
Promise.resolve().then(originalScheduler);
}
};
}
// 立即执行或获取初始值
if (options.immediate) {
// immediate 模式:立即执行一次回调
scheduler();
} else {
// 首次运行 effect,获取旧值
oldValue = _effect.run();
}
// 返回停止函数
return () => {
_effect.stop();
if (cleanup) {
cleanup();
}
};
}
回调调度时机控制
flush 配置原理
Vue3 提供了三种调度时机:
// 1. 'pre':组件更新前执行(默认)
watch(source, callback, { flush: 'pre' });
// 2. 'post':组件更新后执行
watch(source, callback, { flush: 'post' });
// 3. 'sync':同步执行
watch(source, callback, { flush: 'sync' });
实现原理
// 调度器工厂
function createScheduler(effect, cb, options) {
const { flush } = options;
if (flush === 'sync') {
// 同步:直接执行
return () => {
effect.run();
};
}
// 异步队列
const queue = [];
let isFlushing = false;
function queueFlush() {
if (!isFlushing) {
isFlushing = true;
if (flush === 'post') {
// 组件更新后执行(使用微任务)
Promise.resolve().then(() => {
flushJobs();
});
} else {
// 'pre':组件更新前执行
// 简化处理,也使用微任务
Promise.resolve().then(() => {
flushJobs();
});
}
}
}
function flushJobs() {
try {
queue.forEach(job => job());
} finally {
queue.length = 0;
isFlushing = false;
}
}
return () => {
if (!queue.includes(cb)) {
queue.push(cb);
queueFlush();
}
};
}
深度监听(deep)的实现
递归遍历实现
/**
* 深度遍历对象,收集所有嵌套属性作为依赖
*/
function traverse(value, seen = new Set()) {
// 基本类型或已访问过,直接返回
if (!isObject(value) || 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(v, seen);
traverse(k, seen);
});
}
// 处理 Set
else if (value instanceof Set) {
value.forEach(v => traverse(v, seen));
}
// 处理普通对象
else {
for (const key in value) {
if (value.hasOwnProperty(key)) {
traverse(value[key], seen);
}
}
}
return value;
}
深度监听带来的性能问题
const state = reactive({
user: {
profile: {
name: '张三',
address: {
city: '北京',
street: '长安街'
}
}
}
});
// 不推荐:深度监听大对象
watch(state, () => {
console.log('state 变化了');
}, { deep: true });
// 推荐:只监听需要的路径
watch(() => state.user.profile.name, (newVal) => {
console.log('name 变化了', newVal);
});
// 或者使用 watchEffect 自动追踪
watchEffect(() => {
console.log(state.user.profile.name); // 只依赖 name
});
清理机制(onInvalidate)
为什么需要清理?
// 异步操作的竞态问题
watch(id, async (newId, oldId) => {
const data = await fetchData(newId);
// 问题:如果 id 快速变化,前一个请求可能覆盖后一个
renderData(data);
});
onInvalidate 的实现
function watch(source, cb, options = {}) {
let cleanup;
function onInvalidate(fn) {
cleanup = fn;
}
const scheduler = () => {
// 执行清理
if (cleanup) {
cleanup();
}
const newValue = getter();
// 调用回调,传入新旧值和 onInvalidate
cb(newValue, oldValue, onInvalidate);
oldValue = newValue;
};
// 在停止监听时也要清理
const stop = () => {
_effect.stop();
if (cleanup) {
cleanup();
}
};
return stop;
}
实战:watch 的高级用法
监听多个数据源
const firstName = ref('张');
const lastName = ref('三');
const age = ref(25);
// 监听数组
watch([firstName, lastName], ([newFirst, newLast], [oldFirst, oldLast]) => {
console.log(`姓名从 ${oldFirst}${oldLast} 变为 ${newFirst}${newLast}`);
});
// 监听 getter 返回的对象
watch(
() => ({
name: `${firstName.value}${lastName.value}`,
age: age.value
}),
(newVal, oldVal) => {
console.log('用户信息变化:', newVal, oldVal);
},
{ deep: true }
);
实现防抖 watch
function debouncedWatch(source, cb, delay = 300, options = {}) {
let timeoutId;
const debouncedCb = (newVal, oldVal) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
cb(newVal, oldVal);
}, delay);
};
return watch(source, debouncedCb, options);
}
// 使用
const searchText = ref('');
const stop = debouncedWatch(searchText, (newVal) => {
console.log('发送搜索请求:', newVal);
}, 500);
实现可暂停的 watch
function pausableWatch(source, cb, options = {}) {
let isPaused = false;
const pause = () => {
isPaused = true;
};
const resume = () => {
isPaused = false;
};
const wrappedCb = (newVal, oldVal) => {
if (!isPaused) {
cb(newVal, oldVal);
}
};
const stop = watch(source, wrappedCb, options);
return {
stop,
pause,
resume
};
}
// 使用
const count = ref(0);
const { pause, resume } = pausableWatch(count, (newVal) => {
console.log('count 变化:', newVal);
});
count.value = 1; // 输出
pause();
count.value = 2; // 不输出
resume();
count.value = 3; // 输出
实现条件 watch
function conditionalWatch(source, condition, cb, options = {}) {
return watch(source, (newVal, oldVal) => {
if (condition(newVal, oldVal)) {
cb(newVal, oldVal);
}
}, options);
}
// 使用
const count = ref(0);
conditionalWatch(count,
(newVal, oldVal) => newVal > oldVal && newVal % 2 === 0,
(newVal) => {
console.log('count 增加了且为偶数:', newVal);
}
);
count.value = 1; // 不输出
count.value = 2; // 输出
count.value = 3; // 不输出
count.value = 4; // 输出
性能优化与最佳实践
1. 精确依赖
// 不推荐:依赖太多
watch(user, () => {
console.log(user.value.name, user.value.age);
}, { deep: true });
// 推荐:只依赖需要的属性
watch(() => user.value.name, (name) => {
console.log(name);
});
watch(() => user.value.age, (age) => {
console.log(age);
});
2. 避免副作用累积
// 问题:每次执行都创建新监听
watch(id, (newId) => {
// 每次 id 变化都会创建一个新的 watch
watch(detailId, () => {
// ...
});
});
// 正确:使用 onInvalidate 清理
watch(id, (newId, oldId, onInvalidate) => {
let stopDetailWatch;
onInvalidate(() => {
if (stopDetailWatch) {
stopDetailWatch();
}
});
stopDetailWatch = watch(detailId, () => {
// ...
});
});
3. 合理使用 immediate
// 需要初始值:使用 immediate
watch(source, (newVal) => {
initializeWithValue(newVal);
}, { immediate: true });
// 不需要初始值:默认懒执行
watch(source, (newVal) => {
handleChange(newVal);
});
4. 选择正确的时机
// 需要访问更新后的 DOM:使用 post
watch(source, () => {
console.log('DOM 已更新');
}, { flush: 'post' });
// 需要组件更新前处理:使用 pre
watch(source, () => {
console.log('DOM 即将更新');
}, { flush: 'pre' });
// 需要同步响应:使用 sync
watch(source, () => {
console.log('立即响应');
}, { flush: 'sync' });
常见陷阱与解决方案
陷阱1:异步更新问题
const count = ref(0);
watch(count, (newVal) => {
console.log('Current DOM:', document.getElementById('counter')?.textContent);
// 可能不是最新的
});
// 解决方案:使用 nextTick
watch(count, async (newVal) => {
await nextTick();
console.log('Updated DOM:', document.getElementById('counter')?.textContent);
});
陷阱2:无限循环
const user = ref({ name: 'John' });
// 错误:会触发无限循环
watch(user, (newUser) => {
newUser.name = newUser.name.toUpperCase(); // 修改会触发再次监听
}, { deep: true });
// 正确:使用拷贝
watch(user, (newUser) => {
const updated = { ...newUser, name: newUser.name.toUpperCase() };
user.value = updated; // 只在必要时更新
});
陷阱3:对象引用丢失
const state = reactive({ user: { name: 'John' } });
// 错误:oldVal 和 newVal 引用相同
watch(() => state.user, (newVal, oldVal) => {
console.log(newVal === oldVal); // true - 同一个对象
});
// 正确:深拷贝比较
watch(() => ({ ...state.user }), (newVal, oldVal) => {
console.log(newVal === oldVal); // false - 不同对象
});
陷阱4:监听器的执行顺序
const data = ref(0);
// watch 1
watch(data, () => {
console.log('Watch 1');
});
// watch 2
watch(data, () => {
console.log('Watch 2');
});
// watchEffect
watchEffect(() => {
console.log('WatchEffect:', data.value);
});
data.value = 1;
// 输出顺序取决于创建顺序和 flush 配置
陷阱5:内存泄漏
// 错误:在组件内创建但不清理
export default {
setup() {
const data = ref(0);
// 组件销毁后仍然存在
watch(data, () => {
console.log('Changed');
});
}
}
// 正确:使用 onUnmounted 清理
export default {
setup() {
const data = ref(0);
const stop = watch(data, () => {
console.log('Changed');
});
onUnmounted(() => {
stop();
});
}
}
结语
watch 和 watchEffect 是 Vue3 响应式系统中面向开发者的核心 API,它们基于底层的 effect 系统,提供了更友好、更强大的数据监听能力。
对于文章中错误的地方或有任何疑问,欢迎在评论区留言讨论!