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

基于父组件状态移除Ref绑定事件监听器的React音频播放器问题

问题分析

你的核心问题出在事件监听的移除逻辑失效以及组件状态未在曲目切换时正确重置:

  1. onTimeUpdate函数每次组件渲染都会重新创建,导致removeEventListener找不到对应的函数实例,无法移除旧监听
  2. useEffect未设置清理函数,组件卸载或依赖变化时无法自动移除监听
  3. 当曲目切换(当前组件isActive变为false)时,仅修改了isPlaying,但未重置mediaTime,且监听移除时机不对
解决方案

1. 用useCallback缓存事件处理函数

确保onTimeUpdate函数的引用稳定,让removeEventListener能正确匹配到之前添加的监听:

const onTimeUpdate = useCallback(() => {
  if (ref.current) {
    setMediaTime(ref.current.currentTime);
  }
}, [ref]);

2. 重构useEffect,添加清理函数

修改useEffect逻辑,在组件卸载或依赖变化时自动移除监听,同时在isActive变为false时重置组件状态:

useEffect(() => {
  const audioElement = ref.current;
  if (!audioElement) return;

  // 当前曲目激活时添加监听
  if (isActive) {
    audioElement.addEventListener("timeupdate", onTimeUpdate);
  } else {
    // 当前曲目失活时,重置状态并移除监听
    setIsPlaying(false);
    setMediaTime(0);
    audioElement.removeEventListener("timeupdate", onTimeUpdate);
  }

  // 清理函数:组件卸载或依赖变化时强制移除监听
  return () => {
    audioElement.removeEventListener("timeupdate", onTimeUpdate);
  };
}, [isActive, onTimeUpdate]);

3. 优化togglePlaying的逻辑

修复状态更新与播放控制的异步问题,确保状态更新后再执行播放/暂停操作:

const togglePlaying = () => {
  if (!ref.current) return;

  if (!isActive) {
    // 切换到新曲目
    ref.current.src = link;
    ref.current.load();
    setActiveAudio(index);
    setIsPlaying(true);
    ref.current.play();
  } else {
    // 当前曲目切换播放/暂停
    const newIsPlaying = !isPlaying;
    setIsPlaying(newIsPlaying);
    newIsPlaying ? ref.current.play() : ref.current.pause();
  }
};

4. 统一组件命名(可选)

注意你代码里组件定义为AudioPlayer,但父组件中使用的是AudioTrack,统一命名可避免混淆。

完整修正后的AudioTrack组件示例

const AudioTrack = forwardRef(
  ({ index, link, isActive, setActiveAudio }: AudioPlayerProps, ref: RefObject<HTMLAudioElement>) => {
    const [isPlaying, setIsPlaying] = useState(false);
    const [mediaTime, setMediaTime] = useState(0);

    const onTimeUpdate = useCallback(() => {
      if (ref.current) {
        setMediaTime(ref.current.currentTime);
      }
    }, [ref]);

    const togglePlaying = () => {
      if (!ref.current) return;

      if (!isActive) {
        ref.current.src = link;
        ref.current.load();
        setActiveAudio(index);
        setIsPlaying(true);
        ref.current.play();
      } else {
        const newIsPlaying = !isPlaying;
        setIsPlaying(newIsPlaying);
        newIsPlaying ? ref.current.play() : ref.current.pause();
      }
    };

    useEffect(() => {
      const audioElement = ref.current;
      if (!audioElement) return;

      if (isActive) {
        audioElement.addEventListener("timeupdate", onTimeUpdate);
      } else {
        setIsPlaying(false);
        setMediaTime(0);
        audioElement.removeEventListener("timeupdate", onTimeUpdate);
      }

      return () => {
        audioElement.removeEventListener("timeupdate", onTimeUpdate);
      };
    }, [isActive, onTimeUpdate]);

    return (
      <div className={styles.playerContainer}>
        <p>{`${isPlaying}`}</p>
        <button className={styles.playButton} onClick={togglePlaying}>
          {isPlaying ? (
            <img
              className={styles.playIcon}
              src={pauseSrc}
              alt="暂停按钮"
              loading="eager"
            />
          ) : (
            <img
              className={styles.playIcon}
              src={playSrc}
              alt="播放按钮"
              loading="eager"
            />
          )}
        </button>
        <div className={styles.timeValue}>{formatTime(mediaTime)}</div>
      </div>
    );
  }
);

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.06 23:30:58