Vue 3 页面返回时温度场无法清除的问题分析与解决思路
一、问题现象
在页面中开启温度场后,点击返回按钮,前几次通常可以正常清除温度场。但是当用户多次进入温度场页面、开启温度场并返回之后,后续会出现温度场无法关闭、无法清除的问题。
具体表现为:
用户点击返回离开页面后,页面本身已经正常退出,但三维场景中的温度场仍然残留在引擎中。再次进入页面或再次操作时,温度场状态可能与页面 UI 状态不一致,造成"页面认为已经关闭,但场景中仍然显示"的异常。
这个问题不是单纯的接口调用失败,而是页面状态、Vue 组件生命周期、watch 执行时机以及场景资源清理逻辑之间产生了竞态条件。
二、相关逻辑
当前温度场的显示和隐藏主要依赖 showTempField 这个响应式变量。
当 showTempField.value 为 true 时,向引擎发送创建温度场事件:
TemperatureFieldXZ-Create
当 showTempField.value 为 false 时,向引擎发送隐藏温度场事件:
TemperatureFieldXZ-Hide
这个逻辑通常通过 watch 监听 showTempField 的变化实现。
与此同时,页面返回时会执行页面重置逻辑,例如在 usePageBackReset 中将页面状态重置,使 showTempField.value 变成 false。理论上,这应该触发 watch,然后发送隐藏温度场事件。
但是在实际返回过程中,组件可能很快进入卸载阶段,导致 watch 回调没有稳定执行,最终隐藏事件没有发送出去。
三、问题原因
该问题的核心原因是:组件卸载时依赖了已经被外部重置过的响应式状态,导致场景资源清理判断失效。
返回页面时,执行顺序大致如下:
-
用户点击返回按钮。
-
页面返回逻辑开始执行。
-
usePageBackReset重置页面状态。 -
showTempField.value被设置为false。 -
理论上 watch 应该监听到变化并发送
TemperatureFieldXZ-Hide。 -
但是组件同时进入路由切换和卸载流程。
-
Vue 在组件卸载时会清理尚未执行或未完成的响应式副作用。
-
watch 中的隐藏逻辑可能来不及执行。
-
onBeforeUnmount开始执行兜底清理。 -
但此时
showTempField.value已经是false。 -
清理逻辑判断当前温度场不是激活状态,于是跳过隐藏事件。
-
最终
TemperatureFieldXZ-Hide没有发送,温度场残留在引擎中。
这就导致一个结果:
页面状态已经变成 false,但引擎里的温度场资源仍然存在。
也就是说,UI 状态和引擎资源状态发生了不一致。
四、为什么前几次可能正常,后面才出现问题
这个问题具有一定的偶发性和路径依赖。
在某些情况下,如果用户在页面内有过交互,返回逻辑可能会先执行软重置,例如刷新当前组件、重置部分状态,然后再真正离开页面。在这种路径下,watch 有机会执行完成,因此隐藏事件能够正常发送。
但是在另一种情况下,用户直接返回离开页面,组件很快被销毁,watch 回调可能还没有真正完成,组件就已经进入卸载流程。此时如果卸载清理又依赖 showTempField.value,就会因为这个值已经提前被重置为 false,从而误判为"不需要清理"。
所以这个 Bug 并不是每次都会稳定复现,而是在多次进入、退出、状态切换之后更容易暴露出来。
五、问题本质
这个问题的本质是:不应该用页面 UI 状态来判断引擎资源是否需要清理。
showTempField 表示的是页面上的开关状态,它是一个 UI 层状态。它可能会因为页面重置、路由返回、组件刷新等原因被提前修改。
但是引擎中的温度场是否已经创建、是否需要销毁,是场景资源状态。这个状态不应该完全依赖外部传入的响应式变量。
换句话说:
showTempField.value === false
只能说明页面当前希望温度场是关闭的,并不能说明引擎里的温度场已经真的被关闭。
因此,如果 onBeforeUnmount 仍然使用 unref(active) 或 showTempField.value 来判断是否需要清理,就存在误判风险。
六、解决思路
更合理的方案是在封装温度场资源控制的 hook 内部维护一个独立的资源状态,例如:
let resourceActive = false
这个变量不表示 UI 是否选中,也不表示用户是否希望显示温度场,而是表示:
当前 hook 是否已经向引擎发送过 Create,并且还没有成功发送 Hide。
推荐的状态维护逻辑如下:
当发送创建事件成功后:
resourceActive = true
当发送隐藏事件成功后:
resourceActive = false
组件卸载时,不再判断外部传入的 active,而是判断内部的 resourceActive:
onBeforeUnmount(() => {
if (leaveOnUnmount && resourceActive) {
leaveInner()
}
})
这样即使页面返回时 showTempField.value 已经被提前重置成 false,hook 内部仍然知道自己曾经创建过温度场资源,并且还没有完成清理,于是可以在组件卸载时补发隐藏事件。
七、优化后的设计原则
优化后的设计重点是将 UI 状态和资源状态分离。
showTempField 负责表达用户界面上的状态:
用户是否勾选了显示温度场
而 resourceActive 负责表达引擎资源的真实状态:
引擎中是否仍然存在由当前 hook 创建的温度场资源
两者不能简单等价。
页面状态可以被重置,但资源状态必须由资源创建和销毁动作来维护。只有这样,才能避免组件卸载、路由跳转、异步 watch、任务队列之间产生竞态时出现资源泄漏。
八、最终结论
这个问题不是温度场接口本身的问题,而是 Vue 组件生命周期和异步 watch 机制下的资源清理竞态问题。
原来的逻辑在组件卸载时依赖 showTempField 判断是否需要清理温度场,但 showTempField 在页面返回流程中可能已经被提前重置为 false。这会导致卸载清理逻辑误以为温度场已经关闭,从而跳过隐藏事件。
解决这个问题的关键是:在资源 hook 内部维护真实的资源激活状态,不再依赖外部 UI 状态进行卸载清理判断。
也就是说,页面状态负责控制显示意图,资源状态负责保证引擎清理。只有把这两类状态分开,才能彻底解决多次进入和返回后温度场残留的问题。