React使用MediaRecorder录制音频时实例一段时间后变undefined
问题根因
你遇到的变量丢失问题,本质是对React函数组件的重渲染机制理解有误:
- 直接在函数组件顶层声明的普通变量(比如你写的
let audioRecording、const audioData = []),生命周期仅存在于单次组件渲染周期内。 - 只要组件触发重渲染(无论是调用setState更新自身状态、父组件重渲染传导、还是开发环境严格模式的重复执行),组件函数就会完整重新执行一遍,这些普通变量会被重新初始化,之前存储的MediaRecorder实例引用会被直接覆盖为初始值
undefined,数组也会被重置为空。
你观察到的两个现象完全符合这个逻辑:
- 调用setState后1秒内就出问题:setState直接触发了重渲染,变量立刻被重置。
- 3-6分钟长录音才出问题:短时间内没有触发任何重渲染,变量暂时保留;录制几分钟后大概率有其他状态更新、或者React的调度逻辑触发了重渲染,变量就被重置了。
如果把这两个变量声明在组件函数外部,虽然不会被重渲染重置,但会导致多个该组件的实例共享同一份变量,出现录音逻辑互相干扰的bug,同样不可取。
修复方案
使用React提供的useRef存储这类需要跨渲染周期保留、且不需要触发视图更新的可变值。ref对象在组件整个挂载生命周期内引用稳定,挂载在.current属性上的值不会因为重渲染被重置。
完整修正代码如下:
import { useState, useRef, useCallback, useEffect } from 'react'; function AudioRecorder() { const [isRecording, setIsRecording] = useState(false); const [audioFile, setAudioFile] = useState(null); // 用ref存储跨渲染周期保留的实例和数据 const audioRecordingRef = useRef(null); const audioDataRef = useRef([]); const mediaStreamRef = useRef(null); const startRecodingAudio = useCallback(() => { // 录制中禁止重复触发 if (isRecording) return; // 清空上一次录音的残留数据 audioDataRef.current = []; navigator.mediaDevices.getUserMedia({ audio: true }) .then(stream => { mediaStreamRef.current = stream; const recorder = new MediaRecorder(stream); audioRecordingRef.current = recorder; recorder.ondataavailable = e => { audioDataRef.current.push(e.data); }; recorder.onstop = () => { setIsRecording(false); const audioBlob = new Blob(audioDataRef.current, { type: 'audio/wav' }); const audioReader = new FileReader(); audioReader.readAsDataURL(audioBlob); audioReader.onloadend = () => { setAudioFile(audioReader.result); // 释放麦克风资源,关闭权限指示灯 mediaStreamRef.current?.getTracks().forEach(track => track.stop()); }; }; recorder.start(); setIsRecording(true); }) .catch(err => { console.error('录音启动失败:', err); }); }, [isRecording]); const stopRecordingAudio = useCallback(() => { const recorder = audioRecordingRef.current; // 仅在录制状态下执行停止逻辑 if (recorder && recorder.state !== 'inactive') { recorder.stop(); } }, []); // 组件卸载时清理资源,避免内存泄漏 useEffect(() => { return () => { if (audioRecordingRef.current?.state !== 'inactive') { audioRecordingRef.current?.stop(); } mediaStreamRef.current?.getTracks().forEach(track => track.stop()); }; }, []); return ( <div> <div>Play/Stop</div> <div> <button type='button' onClick={startRecodingAudio} disabled={isRecording} > Record </button> <button type='button' onClick={stopRecordingAudio} disabled={!isRecording} > Cancelar </button> <button type='button' onClick={() => console.log(audioRecordingRef.current)} > Check State </button> </div> </div> ); } export default AudioRecorder;
额外优化点
- 给操作按钮增加了
disabled状态判断,避免重复点击、无效点击触发JS报错 - 新增了媒体流资源释放逻辑,解决录音结束后麦克风权限指示灯常亮的问题
- 每次启动录音前清空历史音频数据,避免多次录音的片段被错误拼接
- 用
useCallback包裹事件处理函数,避免子组件不必要的重渲染
内容的提问来源于stack exchange,提问作者Matheus Lasserre
相关产品推荐
相关产品推荐

