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

useEffect用async/await时WebRTC清理报createOffer空值错误如何解决

useEffect中async/await场景下WebRTC资源清理空引用问题修复

问题根因

报错 TypeError: Cannot read properties of null (reading 'createOffer') 的核心原因是 异步执行和组件清理的时序冲突:组件卸载触发清理函数时,pc实例已经被置空,但effect内的异步函数还在执行队列中,等await操作resolve后继续执行到pc.createOffer()等访问pc的逻辑时,就会触发空引用错误。
原有代码存在三个核心缺陷:

  • 异步逻辑没有感知组件卸载状态,组件销毁后仍会继续执行后续WebRTC操作
  • 资源实例作用域不合理,清理逻辑和异步逻辑共享外层可变的pc变量,置空后异步流程无校验直接访问
  • 清理逻辑不完整,没有在组件卸载时中断未完成的异步流程,也没有完整释放摄像头、麦克风、PeerConnection资源

正确实现代码

useEffect(() => {
  // 组件挂载状态标记,用于阻断卸载后的异步逻辑执行
  let isMounted = true;
  let pc = null;
  let localStream = null;

  const initWebRTC = async () => {
    try {
      pc = new RTCPeerConnection();
      if (!callUserFlag) return;

      // 申请媒体权限
      localStream = await navigator.mediaDevices.getUserMedia({
        audio: true,
        video: true
      });
      // 异步返回后先校验挂载状态,已卸载则直接释放资源终止流程
      if (!isMounted) {
        localStream.getTracks().forEach(track => track.stop());
        pc.close();
        return;
      }

      // 绑定本地流
      localVideoRef.current.srcObject = localStream;
      localStream.getTracks().forEach(track => {
        pc.addTrack(track, localStream);
      });

      // 创建offer前再次校验挂载状态
      if (!isMounted) return;
      const offer = await pc.createOffer();
      await pc.setLocalDescription(offer);

      // 发送信令前最后校验挂载状态
      if (!isMounted) return;
      socket.emit("call user", {
        offer: offer,
        toSocket: socketId
      });
    } catch (err) {
      // 组件卸载触发的错误不需要打印日志
      if (isMounted) {
        console.error("WebRTC初始化失败: ", err);
      }
    }
  };

  initWebRTC();

  // 资源清理函数
  return () => {
    isMounted = false;
    // 关闭PeerConnection
    if (pc) {
      pc.close();
      pc = null;
    }
    // 停止所有媒体轨道,释放摄像头/麦克风权限
    if (localStream) {
      localStream.getTracks().forEach(track => track.stop());
      localStream = null;
    }
    // 清空视频元素绑定的流
    if (localVideoRef.current) {
      localVideoRef.current.srcObject = null;
    }
  };
}, [callUserFlag, socketId]);

关键实现要点

  • 所有await异步节点执行完成后,必须先校验组件挂载状态,组件已卸载时立刻释放已申请的资源,终止后续流程,避免访问已被清理的空实例
  • pc、localStream这类可变资源要定义在useEffect的作用域内部,避免多次渲染、多个effect实例共享外层变量导致的状态混乱
  • 资源清理必须覆盖所有申请的资源:RTCPeerConnection必须调用close()方法,媒体轨道必须调用stop()方法,否则会出现摄像头/麦克风常驻占用、页面内存泄漏的问题
  • useEffect的依赖数组需要加入内部用到的callUserFlag、socketId变量,避免闭包捕获旧值导致逻辑异常

内容的提问来源于stack exchange,提问作者Tytus Bomba

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.28 21:06:33