先记住这个答案
浏览器为每个参与过渡的属性独立触发 transitionend 事件。事件对象的 propertyName 属性告诉你是哪个属性完成了过渡,elapsedTime 告诉你这段过渡花了多少秒。如果过渡中途被取消,比如在动画进行中把触发过渡的 class 移除了,浏览器不会触发 transitionend,而是触发 transitioncancel。
propertyName标识结束的过渡属性- 多属性过渡会触发多次,每个属性各一次
- 过渡被打断触发的是
transitioncancel transitionend冒泡,可在容器监听
一个按钮同时过渡两个属性的场景
假设一个按钮 hover 时同时改变 opacity 和 transform,过渡时长不同。鼠标移上去后会先后收到两次 transitionend,快的先到,慢的后到。在 JS 里如果只想在「全部过渡完成」后做某件事,就需要借助 propertyName 来判断。
.btn {
opacity: 1;
transform: scale(1);
/* opacity 200ms 先结束,transform 400ms 后结束 */
transition: opacity 200ms ease, transform 400ms ease;
}
.btn:hover {
opacity: 0.8;
transform: scale(1.05);
}这段 CSS 在 hover 时会产生两次 transitionend。如果你在 JS 里用 addEventListener('transitionend', handler) 监听,handler 会被调用两次。第一次 event.propertyName 是 opacity,第二次是 transform。想等全部结束再做动作,最简单的办法是只判断最后结束的那个属性名。
过渡取消时事件的变化
过渡进行到一半时,如果触发过渡的条件消失了,浏览器会触发 transitioncancel,然后属性值会立刻(或反向过渡)回到初始状态。这时不会再触发 transitionend。
比如一个展开面板,过渡到一半时用户点了收起按钮。代码里如果用 transitionend 来做「展开完成后插入内容」之类的操作,这个回调就永远不会执行。所以依赖 transitionend 做流程控制的代码,必须同时处理 transitioncancel,否则状态会卡住。
el.addEventListener('transitionend', onDone)
el.addEventListener('transitioncancel', onDone) // 取消时也要清理状态
function onDone(e) {
if (e.propertyName !== 'transform') return // 只关心 transform
el.removeEventListener('transitionend', onDone)
el.removeEventListener('transitioncancel', onDone)
// 做后续操作
}同时监听两个事件,保证无论过渡正常结束还是被取消,后续逻辑都能执行。
事件冒泡带来的便利和陷阱
transitionend 是会冒泡的。在一个列表容器上监听,所有子元素的过渡结束都会冒到容器上来。这在做列表动画时很方便,不用给每个子元素单独绑事件。但反过来说,如果容器自身也有过渡,就会混在一起。判断事件来源时用 event.target 确认是哪个元素触发的。
容易答错的地方
- 以为过渡取消后还会触发 transitionend
- 不会。过渡被取消后触发的是
transitioncancel。如果代码里只监听了transitionend来做状态切换,取消场景下状态就会卡死。 - 以为简写属性名会出现在 propertyName 里
- 写
transition: all .3s,触发的事件里propertyName不会是all,而是每个实际发生变化的单一属性名,比如background-color而不是background。
面试官还会怎么问?
transitionstart 和 transitionrun 有什么区别?
transitionrun 在过渡被创建时就触发,即使有 transition-delay 也会立刻触发。transitionstart 要等到 delay 结束、过渡真正开始播放时才触发。如果 delay 是 0,两者几乎同时。
怎么用 Promise 封装等待过渡结束?
可以写一个函数,内部 new Promise,在 resolve 里移除监听。注意要同时处理 transitioncancel 的情况,否则 Promise 可能永远 pending。
animation 的事件和 transition 的事件有什么不同?
@keyframes 动画用的是 animationend、animationstart、animationiteration 这组事件。animationiteration 在每次循环结束时触发,这是 transition 没有的,因为 transition 不支持循环播放。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。