先记住这个答案
watchEffect 的函数参数是清理注册函数,不提供统一的新旧值,因为它可能读取多个响应式状态,也没有要求返回某个被观察的业务值。需要比较前后值时,通常改用 watch 明确指定一个源或多个源;若必须保留 watchEffect,则自行定义上一次快照及首次执行规则。对象历史还需要主动保存相关字段,单纯缓存对象引用不能还原修改前的内容。
- watchEffect 参数不是 newValue 和 oldValue
- 前后比较优先显式指定 watch 源
- 历史对象需要明确快照策略
没有旧值是接口模型的结果
watchEffect 可以读取一个编号、多个开关和嵌套对象,还可以根据条件执行不同分支。框架并没有一个固定返回结果可作为 newValue,更无法为调用者猜测哪些字段应该被保存为上一次的业务状态。
如果写成两个形参并以为它们分别是新旧值,实际取得的第一个仍是清理函数,第二个没有预期含义。先检查函数签名,比对 undefined 做大量兜底更能直接解决问题。
把比较对象写成显式多源
例如资源编号和语言共同确定一个详情请求,使用数组源可以让回调同时得到对应顺序的新旧数组。下例记录一轮变化前后的组合,适合判断是资源切换还是语言切换导致更新。
示例没有开启 immediate,因此第一条记录来自真正的源变化。若需要初次执行,请单独处理尚无有效历史的情况,不要把首次回调里的旧数组或 undefined 当成用户之前实际选择过的值。
import { ref, watch } from 'vue'
const id = ref('A')
const locale = ref('zh')
const changes: string[] = []
const stop = watch([id, locale], ([nextId, nextLocale], [oldId, oldLocale]) => {
changes.push(oldId + ':' + oldLocale + ' -> ' + nextId + ':' + nextLocale)
})同一段同步代码把编号改为 B、语言改为 en,等待刷新后得到一条从 A:zh 到 B:en 的记录。它描述本次批量观察的前后状态,不会逐条列出中间只改了一半的组合。
自行缓存时要定义提交时点
如果效果中计算出的结果需要与上次比较,可以在同步部分形成明确的普通值快照,并在本轮处理结束后更新历史。首次运行应使用显式标记区分没有历史,避免把合法的空字符串或零误判成未初始化。
如果历史表示上一次成功保存到服务端的状态,就应在保存成功后推进,而不是每次效果启动就覆盖。异步结果还可能乱序完成,需要把历史提交与当前任务有效性绑定,防止旧任务改写新的比较基线。
容易答错的地方
- 把对象引用保存为旧快照
- 对象被原地修改后,缓存的引用也指向同一个对象,后续比较看不到真正的历史字段。只提取需要比较的原始字段通常更直接,完整复制则要考虑成本及数据类型的可复制性。
- 把侦听记录当成完整操作历史
- 默认批量调度可能合并多次赋值,最终只观察到净变化。若需要记录每次用户输入或业务命令,应在相应事件入口记录,而不是为补日志把所有监听都改为同步执行。
面试官还会怎么问?
比较两个字段时要创建两个 watch 吗?
不一定。数组源可以一次表达共同决定结果的多个字段,并提供顺序对应的新旧数组。若两个字段属于同一次操作,统一观察还可以减少分别触发时读取到半更新状态的困惑。
深度 watch 能自动提供修改前的对象吗?
不能。深度观察负责触及嵌套依赖,不意味着对整个对象制作每次更新前的副本。原地变更时新旧参数可能是同一引用,需要由应用选择字段快照或不可变替换等历史表示。
自己缓存上一次值有什么额外风险?
需要自行处理初始状态、对象共享、批量更新及异步完成顺序。缓存何时推进也必须符合业务含义:上一次读取、上一次展示和上一次成功保存,可能是三个不同的状态。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。