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

React自定义usePlayer钩子无明显无限循环却触发过多重渲染问题

问题根因分析
  • useSelector 取值范围不合理。你当前用useSelector(state => state.player)取了整个player slice的状态对象,Redux 的 useSelector 默认用引用对比判断是否触发重渲染,只要 player 状态有任何更新都会触发本钩子重渲染,增加了不稳定因素,应该只取需要的currentSlug字段即可。
  • useEffect 依赖缺失。你在 useEffect 中用到了__USE_PLAYER_SLUG变量,但没有加入依赖数组,会导致闭包陷阱,拿到的永远是第一次渲染时的 slug 值,判断是否挂载的逻辑会出错。
  • 全局 Audio 事件没有清理逻辑。你每次调用onLoad绑定loadedmetadata事件时,没有在组件卸载/ slug 变化时清理事件,会导致同一个事件被多次绑定,回调函数多次触发,重复 dispatch mount 动作。
  • 多实例冲突导致循环。因为你全局只有一个 Audio 实例,如果页面上同时存在多个调用usePlayer的组件绑定不同 slug,会出现A组件挂载把currentSlug设为slugA,触发B组件的useEffect判断不匹配把自己的mounted设为false,如果B组件有逻辑触发mount,又会把currentSlug改成slugB,反过来触发A组件更新,形成无限循环。
  • 缺少重复挂载判断。你调用mount的时候没有判断当前Audio的src是否已经是当前slug对应的资源,哪怕已经加载过了也会重新设置src、重新绑定事件、重新dispatch,触发不必要的状态更新。
修复方案

1. 修正基础依赖逻辑

import * as Player from "playerSlice.js";
import { useMemo, useCallback, useEffect } from "react";

const usePlayer = (slug) => {
    // 用useMemo缓存trim后的结果,避免slug没变也生成新值
    const __USE_PLAYER_SLUG = useMemo(() => slug.trim(), [slug]);

    // 只取需要的currentSlug字段,避免不必要的重渲染
    const currentSlug = useSelector(state => state.player.currentSlug);
    const dispatch = useDispatch();

    const [state, setState] = useReducer((state, newState) => ({...state, ...newState}), {
        playing: false
    });

    // 用useCallback缓存事件绑定逻辑,新增清理返回值
    const onLoad = useCallback((callback) => {
        const ready = () => {
            callback();
            __GLOBAL_AUDIO.removeEventListener("loadedmetadata", ready);
        };
        __GLOBAL_AUDIO.addEventListener("loadedmetadata", ready);
        return () => __GLOBAL_AUDIO.removeEventListener("loadedmetadata", ready);
    }, []);

    // 新增重复挂载判断,避免重复修改src
    const mount = useCallback((callback) => {
        const targetSrc = cdn(`/track/audio/${__USE_PLAYER_SLUG}.mp3`);
        if (__GLOBAL_AUDIO.src === targetSrc) {
            callback();
            dispatch(Player.mount(__USE_PLAYER_SLUG));
            return;
        }
        __GLOBAL_AUDIO.src = targetSrc;
        onLoad(() => {
            callback();
            dispatch(Player.mount(__USE_PLAYER_SLUG));
        });
    }, [__USE_PLAYER_SLUG, dispatch, onLoad]);

    // 直接用全局currentSlug判断是否挂载,避免本地状态和全局状态不一致
    const ensureMounted = useCallback((callback) => {
        if(__USE_PLAYER_SLUG === currentSlug){
            callback();
        } else {
            mount(callback);
        }
    }, [__USE_PLAYER_SLUG, currentSlug, mount]);

    const play = useCallback(() => ensureMounted(() => {
        __GLOBAL_AUDIO.play();
        setState({ playing: true });
    }), [ensureMounted]); 

    const pause = useCallback(() => ensureMounted(() => {
        __GLOBAL_AUDIO.pause();
        setState({ playing: false });
    }), [ensureMounted]); 

    const toggle = useCallback(() => ensureMounted(() => {
        if(!state.playing){
            play();
        } else {
            pause();
        }
    }), [ensureMounted, state.playing, play, pause]);

    // 补全useEffect所有依赖
    useEffect(() => {
        // 组件卸载时如果当前挂载的是本slug,清理全局状态避免残留
        return () => {
            if (currentSlug === __USE_PLAYER_SLUG) {
                __GLOBAL_AUDIO.pause();
                dispatch(Player.mount(null));
            }
        };
    }, [currentSlug, __USE_PLAYER_SLUG, dispatch]);

    return {
        playing: state.playing,
        toggle,
        play,
        pause,
    };
};

2. playerSlice无需修改,保持原有逻辑即可

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.06 22:36:01