先记住这个答案
CSS 动画的生命周期有三个事件可以监听。animationstart 在 animation-delay 倒计时结束、动画真正开始播放那一刻触发,不管循环几次只触发一次。animationend 在所有循环全部播完之后触发。animationiteration 在前一轮结束并开始下一轮的瞬间触发,它只在两轮之间出现,第一轮开始时不算,最后一轮结束时也不算。
animationstart等延迟结束才触发animationend在全部循环播完后触发animationiteration只在两轮之间触发- 无限循环动画不会触发
animationend
触发时序和延迟的关系
假设一个动画设了 animation-delay: 1s 和 animation-iteration-count: 3,每轮时长 2 秒。浏览器的执行时间线是这样的:0 到 1 秒什么事件都没有;1 秒时触发 animationstart;3 秒时第一轮结束进入第二轮,触发 animationiteration;5 秒时第二轮结束进入第三轮,再触发一次 animationiteration;7 秒时第三轮播完,触发 animationend。
<div class="box">动画元素</div>
<style>
.box {
animation: slide 2s ease 1s 3; /* 延迟1秒, 每轮2秒, 共3轮 */
}
@keyframes slide {
from { transform: translateX(0); }
to { transform: translateX(200px); }
}
</style>
<script>
const box = document.querySelector('.box');
box.addEventListener('animationstart', e => {
console.log('start', e.elapsedTime); // 0
});
box.addEventListener('animationiteration', e => {
console.log('iter', e.elapsedTime); // 2, 4
});
box.addEventListener('animationend', e => {
console.log('end', e.elapsedTime); // 6
});
</script>注意 elapsedTime 不包含 animation-delay 那段等待时间,它只计动画真正播放了多久。
负延迟和无限循环的特殊情况
如果 animation-delay 设成负值,比如 -1s,动画会跳过开头的 1 秒立刻开始播放。这时 animationstart 仍然会触发,但 elapsedTime 是 1 而不是 0,表示已经跳过了 1 秒的进度。
设了 animation-iteration-count: infinite 的动画永远不会触发 animationend。如果你的逻辑依赖 animationend 来做清理,遇到无限循环动画就会出问题。这时候应该用 animationiteration 来代替,在需要的那一轮结束时手动停止动画。
只播放一轮的动画,也就是 animation-iteration-count: 1,不会触发 animationiteration。因为它只在两轮之间出现,一轮的动画只有开始和结束,中间没有「两轮之间」的时刻。
实际场景:动画结束后移除元素
最常见的用法是在 animationend 里移除 DOM 元素或移除动画类名。比如一个 toast 提示做了淡出动画,淡出播完后从 DOM 里删掉。
animationend 清理元素JavaScripttoast.addEventListener('animationend', () => {
toast.remove();
}, { once: true });
toast.classList.add('fade-out');这里用了 { once: true } 让监听器只执行一次,避免元素上有多个动画时多次触发。如果元素同时有好几个动画,animationend 会为每个动画各触发一次,需要通过事件对象的 animationName 属性判断是哪个动画结束了。
容易答错的地方
- 以为
animationstart在样式生效那一刻就触发 - 设了
animation-delay的动画,animationstart要等延迟结束才触发,不是样式一应用就触发。延迟期间元素处于等待状态,想在延迟期间就应用关键帧第一帧的样式,需要配合animation-fill-mode: backwards。 - 把
animationiteration和animationend搞混 animationiteration在两轮之间触发,animationend在全部循环播完后触发。最后一轮结束时只有animationend,没有animationiteration。同样,第一轮开始时只有animationstart,没有animationiteration。
面试官还会怎么问?
如果一个元素上同时有两个动画,事件触发几次?
每个动画独立触发。两个动画各自有自己的 animationstart 和 animationend,通过事件对象的 animationName 属性可以区分是哪个动画触发的。
transition 有没有类似的事件?
有。transition 有 transitionstart、transitionend、transitionrun 和 transitioncancel。区别在于 transition 没有循环的概念,所以没有对应 animationiteration 的事件。
动画被中途移除了,还会触发 animationend 吗?
不会。如果在动画播放过程中把 animation 属性去掉或者元素从 DOM 删除,animationend 不会触发。需要做清理的话,要在移除动画的代码里手动处理,不能依赖事件回调。
怎么只监听动画最后一轮结束?
直接用 animationend 就行,它只在全部循环播完后触发一次。如果需要在每一轮结束时做点事但只在最后一轮做不同的事,可以在 animationiteration 里计数,或者监听 animationend 做最后的逻辑。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。