watcheffect 的“深度陷阱”指其依赖收集按需、路径敏感而非全覆盖:仅追踪实际执行中访问的属性,未访问的嵌套字段、条件分支外的属性或数组未读索引均不响应。

watchEffect 的“深度陷阱”不是指它会自动深度监听——而是它对深层对象的依赖收集行为看似深度、实则按需追踪、动态生效,容易让人误以为“改了任意嵌套属性都会触发”,结果却漏响应。关键在于:它只追踪实际执行路径中访问到的属性,而非整个对象树。
依赖收集是“路径敏感”的,不是“结构全覆盖”的
watchEffect 在首次运行时,逐行执行副作用函数,仅把代码里真正读取(.value、.prop、[key])过的响应式字段纳入依赖。没走的分支、未访问的嵌套属性,哪怕在同一个 reactive 对象里,也不会被收集。
- 比如:
const user = reactive({ profile: { name: 'A', settings: { theme: 'dark' } } }) - 若 watchEffect 写的是:
watchEffect(() => console.log(user.profile.name))→ 只依赖user.profile.name,改user.profile.settings.theme不触发 - 若写成:
watchEffect(() => console.log(user.profile?.settings?.theme))→ 且初始profile.settings为 undefined,则整个链路不会被追踪(因可选链短路),后续即使赋值也不会自动补上依赖
条件分支导致依赖“时有时无”
当副作用函数内含 if/else、三元或逻辑运算时,watchEffect 每次运行只会重新收集当前分支访问的依赖。这既是灵活性所在,也是陷阱源头。
watchEffect(() => useAdvanced.value ? api.fetch(user.id) : renderBasic(user.name))- 初始
useAdvanced.value === false→ 只收集user.name;之后切换为 true → 自动停止监听user.name,转而收集user.id和api相关状态 - 但如果你忘了让
user.id在 true 分支中被读取(比如只传参没读取),那它就不会成为依赖,变化也不会触发重执行
数组与对象的“浅引用变更”仍可能逃逸
watchEffect 对 reactive 对象默认是递归追踪其内部所有被访问属性,但这不等于“监听整个对象”。以下操作仍不会触发:
- 直接替换整个嵌套对象:
user.profile = { name: 'B' }→ 触发(因为user.profile本身被访问并赋值) - 但若原写法是
watchEffect(() => user.profile.name),然后你执行user.profile.settings = { lang: 'zh' }→ 不触发(settings没被访问过) - 对 ref 包裹的数组:若只读
list.value[0],改list.value[1]或list.value.push()都不会触发(除非启用deep: true,但 watchEffect 本身不支持该选项)
调试与规避建议
遇到“改了却没反应”,优先检查是否落入上述路径盲区:
- 用
onTrack查看实际收集了哪些依赖:watchEffect((onInvalidate) => { ... }, { onTrack(e) { console.log('tracked:', e.key, 'on', e.target) } }) - 避免过度依赖可选链或未定义兜底逻辑,确保关键字段在每次执行中稳定可访问
- 若确实需要监听整个嵌套结构的变化,更稳妥的方式是显式用
watch(() => obj, ..., { deep: true }),而不是依赖 watchEffect 的自动行为











