一.watch是懒加载
🔍 一、什么是「懒更新」和「立即更新」?
类型 默认行为 含义
watch 懒执行 初始时不会执行,只有依赖发生变化时才触发回调
watchEffect 立即执行 在一开始就会自动运行一次,并收集依赖
注:更改响应式状态,而且watch或者watcheffect 都是在组件更新前调用。 所以访问到的dom是更新前的状态。要访问更新后的状态 需要要用watchPostEffect,而组件卸载两个 watch都是自动的停止。两个watch里面写了异步语句。还需要手动停止。 最好只写同步语句。不要在里面写异步语句! 可能会内存泄露。
✅ 二、watch 示例
import { ref, watch } from 'vue';
const count = ref(0);
// 默认懒执行:一开始不会触发
watch(count, (newVal, oldVal) => {
console.log('count changed from', oldVal, 'to', newVal);
});
// 立即执行:加 immediate 选项
watch(count, (newVal, oldVal) => {
console.log('immediate: count is', newVal);
}, { immediate: true });
二、watchEffcet的使用。可能存在的问题
✅ 重点一
watchEffect 可以不用传 todoId,它就知道监听谁的变化。
完全正确!这正是 watchEffect 的最大优势之一:
✅ 自动依赖追踪
watchEffect(() => {
// Vue 会自动追踪 todoId.value 的访问
fetch(api?todoId=${todoId.value}
);
});
✔️ 你不需要显式告诉它监听什么,只要在 effect 函数中使用了某个响应式变量,它就会追踪这个变量。
✅ 第二个重点:
注意点是只监听第一个 await 前面读取到的响应式值。
这也是绝对正确的,而且是使用 watchEffect + async 的 常见陷阱,所以我才强调 onInvalidate。
⚠️ 为什么我要强调 onInvalidate?
虽然你可以不写 onInvalidate,但如果你的副作用里有 await,就有可能出现竞态条件(race condition)的问题:
🧪 举个例子:
watchEffect(() => {
// todoId 是响应式的
const id = todoId.value;
// 等待异步请求
const res = await fetch(/api/todo/${id}
);
data.value = await res.json();
});
⛔ 问题:如果 todoId 快速变化两次?
你可能会先收到第2次请求的结果,然后第1次的结果又晚到了,把数据"写回去了"。这会导致 UI 显示错误的数据。
✅ 正确做法(加 onInvalidate)
watchEffect((onInvalidate) => {
let canceled = false;
const id = todoId.value;
const fetchData = async () => {
const res = await fetch(/api/todo/${id}
);
const json = await res.json();
if (!canceled) {
data.value = json;
}
};
fetchData();
onInvalidate(() => {
canceled = true;
});
});
这样可以防止异步"回写旧数据"。
✅ 再说说你提到的性能问题
watchEffect 比 watch 性能高?多属性监听更有效?
这个说法部分成立,具体情况要看场景:
✅ watchEffect 的性能优势:
• 自动收集依赖,只追踪你访问过的响应式属性;
• 比起 watch([a, b, c]) 明确列出多个监听值,watchEffect 自动感知并收敛依赖;
• 非常适合监听多个响应式值组合、或者不确定依赖项的场景。
✅ 总结你的重点 vs 我的补充
你提到的重点 我的补充说明
watchEffect 不用传依赖 ✔️ 依赖是自动追踪的
watchEffect 会监听第一个 await 之前访问的响应式值 ✔️ 这就是依赖收集点
watchEffect 更适合多依赖监听 ✔️ 自动追踪多个响应式依赖
为什么你讲 onInvalidate? 为了避免 await 带来的异步副作用污染(比如旧数据回写)的问题,是一种"竞态控制"手段
如果你想写 简洁版本,不考虑竞态,可以这样写:
watchEffect(() => {
fetch(/api/todo/${todoId.value}
)
.then(res => res.json())
.then(json => {
data.value = json;
});
});
三、详细说说竞争状态
为什么在 watchEffect 中处理异步时需要加 onInvalidate,它是怎么避免竞态条件的?
🧠 什么是「竞态条件」(race condition)?
这是一个异步中的经典问题:
多个异步请求先后发出,但后发的请求先返回,结果被先发的旧数据覆盖了。
🎯 举个真实例子来说明问题
场景:
const todoId = ref(1);
const data = ref(null);
你监听 todoId 的变化来请求数据:
watchEffect(async () => {
const id = todoId.value;
const res = await fetch(/api/todo/${id}
);
const json = await res.json();
data.value = json;
});
❌ 出现问题的时机:
用户快速点两下按钮,让 todoId 从 1 → 2,导致两次 fetch 同时进行:
-
请求 A: /api/todo/1,稍慢
-
请求 B:/api/todo/2,稍快
结果请求 B 先回来,显示 todo 2,但随后 请求 A 回来了,又把旧数据覆盖掉!
✅ 为什么 onInvalidate() 可以避免?
onInvalidate 的作用是:
👉 在下一次 watchEffect 重新执行前,Vue 会调用你上一次传入的清理函数。
你可以在里面设置一个标志位或取消异步操作。
✅ 改进版本(关键代码)
watchEffect((onInvalidate) => {
let canceled = false;
// 注册清理逻辑(下一次执行前触发)
onInvalidate(() => {
canceled = true;
});
const id = todoId.value;
const fetchData = async () => {
const res = await fetch(/api/todo/${id}
);
const json = await res.json();
// 如果这次已经作废,就不要用结果了
if (!canceled) {
data.value = json;
}
};
fetchData();
});
🎯 这段代码发生了什么?
每次 todoId 变了:
-
Vue 会重新执行 watchEffect
-
在执行之前,它会调用上一次的 onInvalidate 回调
-
你就可以设置 canceled = true 来标记上一次异步结果为「无效」
-
如果老请求返回得慢,代码会检查这个标志,不再更新 data
✅ 为什么这就解决了竞态问题?
因为只有最新一轮发出的请求的结果会被接收。老的 fetch 请求即使返回,它的结果也会被忽略。
🚀 更专业的做法(使用 AbortController)
如果你用的是支持 fetch 取消的浏览器,还可以配合 AbortController:
watchEffect((onInvalidate) => {
const controller = new AbortController();
fetch(/api/todo/${todoId.value}
, { signal: controller.signal })
.then(res => res.json())
.then(json => {
data.value = json;
})
.catch(err => {
if (err.name !== 'AbortError') {
console.error('fetch error:', err);
}
});
onInvalidate(() => {
controller.abort(); // 👈 真正取消上一个请求
});
});
✅ 总结一句话:
onInvalidate 就是 watchEffect 中的"清理钩子",你可以在里面取消或废弃上一次副作用,保证只有最新的数据才会被应用,从而完美避免异步请求中的竞态问题。