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
相关产品推荐
相关产品推荐

