React自定义hook中事件监听器调用setState触发无限循环问题排查
问题根因
无限循环来自两个核心隐患的叠加:
- hook内部定义的
_handleLoadedMetadata、_stopAnimation没有做引用稳定化处理,每次hook所在组件重渲染时都会生成全新的函数实例。你当前的useEffect依赖为空数组,只会在首次挂载时注册一次回调,清理函数也仅会在组件卸载时执行,一旦运行过程中回调引用发生变化,旧的回调会残留在Player的回调队列中无法被正确移除。 loadedmetadata事件被反复触发:回调中调用setState会触发组件重渲染,如果你的业务代码中存在重渲染时重复给Audio实例赋值相同src的逻辑,每次赋值都会重新触发loadedmetadata事件,事件执行所有残留回调→再次触发setState→重渲染→再次触发事件,形成死循环。这也刚好解释了为什么去掉_stopAnimation(去掉setState调用)之后循环就会终止。
另外Player类的removeListenerCallback方法存在逻辑漏洞:当传入的回调不在队列中时indexOf会返回-1,直接执行splice(-1, 1)会误删队列最后一个回调,进一步导致回调残留问题。
修复步骤
- 用
useCallback稳定回调函数的引用
const _stopAnimation = useCallback(() => { setState({ allowNextFrame: false, animationRunning: false }); }, []); const _handleLoadedMetadata = useCallback(() => { _stopAnimation(); }, [_stopAnimation]);
- 将稳定后的回调加入useEffect依赖数组,确保回调更新时正确清理旧实例
useEffect(() => { Player.addListenerCallback("loaded", _handleLoadedMetadata); return () => { Player.removeListenerCallback("loaded", _handleLoadedMetadata); }; }, [_handleLoadedMetadata]);
- 修复
removeListenerCallback的边界判断
static removeListenerCallback(event, callback){ const listenerCallbacks = this.#listenerCallbacks; if(listenerCallbacks.hasOwnProperty(event)){ const index = listenerCallbacks[event].indexOf(callback); // 仅当回调存在时才执行删除 if (index !== -1) { this.#listenerCallbacks[event].splice(index, 1); } } }
- 优化音频src赋值逻辑,避免重渲染时重复赋值相同src触发加载事件
// 示例:在赋值src前先做相等判断 useEffect(() => { const audioSrc = `/audio/${slug}.mp3`; // 仅当src变化时才重新赋值 if (slug && Player.#audio.src !== audioSrc) { Player.#audio.src = audioSrc; } }, [slug]);
内容的提问来源于stack exchange,提问作者Grover
相关产品推荐
相关产品推荐

