You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React使用MediaRecorder录制音频时实例一段时间后变undefined

问题根因

你遇到的变量丢失问题,本质是对React函数组件的重渲染机制理解有误:

  • 直接在函数组件顶层声明的普通变量(比如你写的let audioRecording、const audioData = []),生命周期仅存在于单次组件渲染周期内。
  • 只要组件触发重渲染(无论是调用setState更新自身状态、父组件重渲染传导、还是开发环境严格模式的重复执行),组件函数就会完整重新执行一遍,这些普通变量会被重新初始化,之前存储的MediaRecorder实例引用会被直接覆盖为初始值undefined,数组也会被重置为空。

你观察到的两个现象完全符合这个逻辑:

  1. 调用setState后1秒内就出问题:setState直接触发了重渲染,变量立刻被重置。
  2. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 01:39:16