Vue 的响应式更新时机 + 异步录音事件回调造成状态“延后更新”或异步竞态问题(race condition)
这个问题其实是 Vue 的响应式更新时机 + 异步录音事件回调 共同造成的。
你提到的现象是:
切换时 audioUrl 已经被清空了(控制台能看到 undefined),但模板中的 v-if=“audioUrl” 依然能拿到旧值,导致 组件仍然渲染。
这确实是 Vue 中常见的 状态“延后更新”或异步竞态问题(race condition)。
我来详细解释一下原因,然后给出最稳妥的解决方案。
🧩 一、问题原因分析
MediaRecorder.onstop 是异步的
在 stopRecording() 调用时,MediaRecorder 并不会立刻执行 onstop 回调。
浏览器会在音频数据完全处理完后再触发 onstop,这通常比同步逻辑延迟几十毫秒到几百毫秒。
也就是说,流程可能是:
stopAndCleanup() 调用
→ cleanupRecordingResources() 把 audioUrl 清空
→ Vue 准备重新渲染
→ (几毫秒后)MediaRecorder.onstop 被触发
→ 又创建了新的 blob、file、audioUrl
→ audioUrl 被重新赋值(旧值被“复活”)
所以,你看到控制台里 audioUrl 一度清空,但模板里 还显示,
是因为后面又被异步的 onstop 回调写回了旧的 blob 对象。
✅ 二、核心修复思路
在 清理逻辑之前,必须断开或防止 MediaRecorder.onstop 的后续执行。
否则它会在 cleanup 后又创建新的音频文件。
🧠 三、修复方案(推荐实现)
在 cleanupRecordingResources() 开始前,加上:
if (mediaRecorder.value) {
mediaRecorder.value.onstop = null;
mediaRecorder.value.ondataavailable = null;
}
这样可以彻底阻止在 cleanup 后出现新的音频回调。
完整改写如下 👇:
const cleanupRecordingResources = () => {
// 防止异步回调“复活”旧状态
if (mediaRecorder.value) {
mediaRecorder.value.onstop = null;
mediaRecorder.value.ondataavailable = null;
}
// 撤销 URL
if (audioUrl.value) {
try { URL.revokeObjectURL(audioUrl.value); } catch (e) {}
audioUrl.value = undefined;
}
// 停止流
if (recordingStream.value) {
recordingStream.value.getTracks().forEach((t) => t.stop());
recordingStream.value = undefined;
}
// 清空 recorder & chunks & file
mediaRecorder.value = undefined;
recordingChunks.value = [];
recordedAudio.value = null;
isRecording.value = false;
isPaused.value = false;
};
✨ 四、额外建议:提高状态同步的安全性
有时即便如此,UI 仍可能滞后一帧更新(因为 Vue 的 DOM diff 是异步批量处理的)。
可以显式地强制刷新(这在切换时特别有用):
import { nextTick } from “vue”;
const stopAndCleanup = async () => {
stopRecording();
cleanupRecordingResources();
await nextTick(); // 等待 DOM 刷新,确保 v-if 状态立即更新
console.log(“after cleanup”, recordedAudio.value, audioUrl.value);
};
✅ 五、总结
问题 原因 解决方案
切换后 未正确消失 MediaRecorder.onstop 异步回调重新赋值 audioUrl 在 cleanup 前解除事件绑定
UI 仍一帧延迟 Vue 异步渲染机制 调用 await nextTick() 强制同步刷新
父组件清理不生效 子组件内部仍有流或事件引用 在 cleanupRecordingResources() 里彻底释放引用