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

React自定义hook中事件监听器调用setState触发无限循环问题排查

问题根因

无限循环来自两个核心隐患的叠加:

  1. hook内部定义的_handleLoadedMetadata、_stopAnimation没有做引用稳定化处理,每次hook所在组件重渲染时都会生成全新的函数实例。你当前的useEffect依赖为空数组,只会在首次挂载时注册一次回调,清理函数也仅会在组件卸载时执行,一旦运行过程中回调引用发生变化,旧的回调会残留在Player的回调队列中无法被正确移除。
  2. loadedmetadata事件被反复触发:回调中调用setState会触发组件重渲染,如果你的业务代码中存在重渲染时重复给Audio实例赋值相同src的逻辑,每次赋值都会重新触发loadedmetadata事件,事件执行所有残留回调→再次触发setState→重渲染→再次触发事件,形成死循环。这也刚好解释了为什么去掉_stopAnimation(去掉setState调用)之后循环就会终止。

另外Player类的removeListenerCallback方法存在逻辑漏洞:当传入的回调不在队列中时indexOf会返回-1,直接执行splice(-1, 1)会误删队列最后一个回调,进一步导致回调残留问题。

修复步骤

  1. 用useCallback稳定回调函数的引用
const _stopAnimation = useCallback(() => {
  setState({
    allowNextFrame: false,
    animationRunning: false
  });
}, []);

const _handleLoadedMetadata = useCallback(() => {
  _stopAnimation();
}, [_stopAnimation]);
  1. 将稳定后的回调加入useEffect依赖数组,确保回调更新时正确清理旧实例
useEffect(() => {
  Player.addListenerCallback("loaded", _handleLoadedMetadata);
  return () => {
    Player.removeListenerCallback("loaded", _handleLoadedMetadata);
  };
}, [_handleLoadedMetadata]);
  1. 修复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);
    }
  }
}
  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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.08 12:48:02