应使用 deep: true 监听整个对象并结合初始快照做深度比对,而非逐字段监听;需在 onmounted 后获取 structuredclone 快照,标准化 date/数组等类型,保存后同步更新快照,并配合路由守卫拦截未保存离开。

直接监听整个对象并用深度对比判断实质变更,比逐个字段监听更稳、更少误判。关键不在“监听到变化”,而在“变化是否真实影响业务状态”。
用 deep: true + 初始快照做差异判定
Vue 的 watch 默认只响应引用变化,改一个嵌套属性(比如 form.profile.name = '李四')不会触发回调——除非开启 deep: true。但光开 deep 还不够,因为:
- 对象内部 Date、数组、函数等类型变化时,引用变了但内容可能没变(如选日期生成新 Date 实例)
- 用户没改内容,只是触发了 reactive 更新(如重置表单时赋值同一对象)
所以真正可靠的做法是:页面加载后立刻保存一份初始快照(推荐 structuredClone(form)),再用 watch 深度监听整个 form,并在回调里用 isEqual(newVal, initialForm) 判断是否真有业务级差异。
避开首次赋值和引用抖动的坑
watch 在初始化时可能因数据回填自动触发一次,导致 isDirty 错误为 true。解决方法有两个:
- 不依赖
immediate: true,改在onMounted后手动启用监听 - 对易变类型做标准化预处理:Date 转 ISO 字符串,数组排序后再比对,对象忽略非业务字段(如
loading、showPassword)
提交后必须同步更新快照
用户点“保存”成功后,不能只设 isDirty.value = false 就完事。必须同步执行 initialForm = structuredClone(form),否则下次修改就失去比对基准。这步漏掉,整个“未保存提示”逻辑会逐步失效。
结合路由守卫防意外离开
当 isDirty.value === true 且用户点击菜单或输入新 URL 时,用 onBeforeRouteLeave 拦截:
- 调用
next(false)暂停跳转 - 弹出自定义确认框(不用原生
window.confirm,避免被浏览器拦截) - 用户点“离开”,再调用
next()放行;点“取消”,就维持当前页
不复杂但容易忽略。重点在于“初始快照”的获取时机和比对方式是否稳定,而不是监听本身。










