React中是否可以实现用于检测元素动画状态的useIsAnimating Hook?
原实现的核心失效原因
- 事件回调传参错误:代码中
addEventListener('animationstart', setIsAnimating(true))会在effect执行时立刻调用setIsAnimating(true),将该调用的返回值(undefined)作为事件处理器传入,动画实际触发时根本不会执行状态更新逻辑。 - 事件解绑完全失效:
removeEventListener要求传入与绑定时完全一致的函数引用,解绑逻辑中传入的是重新构造的setState调用结果,与当初绑定的空引用不匹配,既无法正确清理监听、造成内存泄漏,也无法在组件卸载时移除副作用。 - 场景覆盖不全:仅绑定了
animationstart和animationend事件,完全没有覆盖CSS过渡(transition)的状态监测,同时遗漏了动画中途取消、重复迭代的边界场景,遇到无限循环动画、多动画并行、动画中途中断的情况时状态判断会完全失真。 - ref绑定时机漏洞:如果effect执行时ref指向的DOM节点还未完成挂载,会直接跳过监听绑定,后续DOM挂载完成后也不会补绑监听逻辑。
修正后的可运行实现
function useIsAnimating(ref) { const [isAnimating, setIsAnimating] = React.useState(false); React.useEffect(() => { const targetEl = ref?.current; if (!targetEl) return; // 重算当前元素的动画运行状态 const checkAnimationState = () => { // 获取当前元素上所有处于运行状态的动画,覆盖CSS动画、CSS过渡类型 const runningAnimations = targetEl.getAnimations({ subtree: false }); setIsAnimating(runningAnimations.length > 0); }; // 绑定所有与动画、过渡状态变化相关的事件 const trackEvents = [ 'animationstart', 'animationend', 'animationcancel', 'animationiteration', 'transitionstart', 'transitionend', 'transitioncancel' ]; trackEvents.forEach(eventName => { targetEl.addEventListener(eventName, checkAnimationState); }); // 初始化时做一次状态校验 checkAnimationState(); // 用与绑定时完全相同的函数引用解绑监听 return () => { trackEvents.forEach(eventName => { targetEl.removeEventListener(eventName, checkAnimationState); }); }; }, [ref]); return isAnimating; }
关键实现逻辑说明
- 所有状态变化事件统一复用同一份检测函数,保证绑定时和解绑时的函数引用完全一致,清理逻辑正常生效
- 借助原生
Element.getAnimations()接口完成状态校验,自动覆盖CSS关键帧动画、CSS过渡、Web Animation API创建的所有运行中动画,不需要手动维护计数器处理多动画并行的场景 - 每次事件触发时重新校验当前元素的所有运行中动画数量,只要存在至少一个未结束的动画,就将状态标记为
true,从根源上避免单个动画结束就错误重置状态的问题 - 移除了effect依赖中多余的
setIsAnimating项,该API是React提供的稳定引用,不需要纳入依赖数组,不会触发不必要的副作用重跑
适配的场景范围
- 无限循环的CSS关键帧动画会被正确识别为持续运行状态,不会因为缺少
animationend事件导致状态卡死 - 同时在元素上并行运行多个动画、过渡的场景,状态判断与实际运行情况保持一致
- 动画被样式变更、用户交互、组件卸载中途打断的场景,会通过
animationcancel/transitioncancel事件触发状态重算,不会出现状态与实际脱节的问题
内容的提问来源于stack exchange,提问作者Paul DeVito
相关产品推荐
相关产品推荐

