替换style.animation后JS动画未重置问题排查与解决
问题描述
我在HTML中设置了两个<span class="submenu">元素,搭配以下JS脚本:
for(let i = 0; i < 2; i++){ document.head.prepend(document.createElement("style")) } const duration = 5 const set_animation = (view_index, submenu, opacity_end) => { const opacity_start = window.getComputedStyle(submenu).getPropertyValue("opacity") document.head.querySelectorAll("style")[view_index].innerHTML = `@keyframes anim${view_index} {0% {opacity: ${opacity_start}} 100% {opacity: ${opacity_end}}}}` submenu.style.animation = `anim${view_index} ${Math.abs(opacity_end - opacity_start) * duration}s forwards` } let i = 0 document.querySelectorAll(".submenu").forEach((submenu) => { let i_ = i submenu.addEventListener("mouseenter", (event) => { console.log("enter", i_) set_animation(i_, submenu, 0) }) submenu.addEventListener("mouseleave", (event) => { console.log("leave", i_) set_animation(i_, submenu, 1) }) i++ })
预期效果:鼠标进入<span.submenu>时,元素不透明度缓慢降至0;鼠标离开时,不透明度缓慢升至1。
实际问题:第一次进入动画正常,离开后视觉上不透明度回到1,但再次进入时,动画直接跳转到对应时间进度的不透明度值,仿佛之前的动画还在后台计时。
注:无法使用CSS:hover实现,实际场景是由元素容器的鼠标进出触发淡出效果。
问题原因
- 动画状态未重置:当重复设置元素的
animation属性时,浏览器会判定为同一动画的重复执行,而非全新动画。它会保留之前动画的时间进度记录,导致再次触发时直接从上次中断的节点继续,而非从头开始。 - 动态关键帧的处理延迟:每次调用
set_animation都会重写对应<style>标签内的关键帧,但浏览器对动态修改的关键帧存在处理延迟,且旧动画的状态未被彻底清除,导致新动画无法正确基于当前实际的透明度值启动。
解决方案
核心思路是每次触发动画前,彻底清除元素的旧动画状态,让浏览器重新识别新动画,同时确保关键帧与动画触发时机匹配。
优化后的代码
const duration = 5; // 为每个submenu分配独立的style元素,避免关键帧互相覆盖 document.querySelectorAll(".submenu").forEach((submenu, index) => { const style = document.createElement("style"); document.head.appendChild(style); const setAnimation = (targetOpacity) => { // 1. 清除当前动画,重置状态 submenu.style.animation = "none"; // 强制浏览器重绘,确保动画状态被完全重置 void submenu.offsetWidth; // 2. 获取元素当前实际的透明度值 const currentOpacity = window.getComputedStyle(submenu).getPropertyValue("opacity"); // 3. 定义新的关键帧 style.innerHTML = ` @keyframes anim${index} { 0% { opacity: ${currentOpacity}; } 100% { opacity: ${targetOpacity}; } } `; // 4. 计算动画时长并启动动画 const animDuration = Math.abs(targetOpacity - currentOpacity) * duration; submenu.style.animation = `anim${index} ${animDuration}s forwards`; }; submenu.addEventListener("mouseenter", () => setAnimation(0)); submenu.addEventListener("mouseleave", () => setAnimation(1)); });
关键改进点
- 清除动画+强制重绘:设置
animation: none后,通过void submenu.offsetWidth触发浏览器重绘,彻底清除旧动画的状态记录,保证新动画从头开始执行。 - 独立style元素:每个子菜单对应专属的
<style>标签,避免多个元素的关键帧互相覆盖,确保动画逻辑独立。 - 简化逻辑结构:将style元素创建、事件绑定整合到循环中,减少全局变量依赖,代码更易维护。
内容的提问来源于stack exchange,提问作者Axel Carrère
相关产品推荐
相关产品推荐

