先记住这个答案
过渡进行中如果目标值被修改了,浏览器会取消当前过渡,以此刻的插值位置为起点,朝新目标发起一段新的过渡。如果新目标恰好是原来的起始值(也就是反向过渡),浏览器会自动缩短新过渡的时长,让回程时间和已经播放的时间成比例,这样来回切换时动画不会有「去时快、回时慢」的不协调感。
- 新过渡从当前插值位置开始,不是从初始值重来
- 反向过渡可能自动缩短剩余时长
- 缩短后的时长等于已播放时长乘以原始时长的比例
- 频繁中断不会叠加多个过渡,只有最新那一个生效
hover 到一半移走鼠标的场景
最典型的例子是按钮 hover 放大。鼠标移上去,按钮从 scale(1) 往 scale(1.1) 过渡,设了 transition: transform 0.4s。如果 0.1 秒后鼠标移走了,按钮此时大概在 scale(1.025) 的位置。
.btn {
transform: scale(1);
transition: transform 0.4s ease;
}
.btn:hover {
transform: scale(1.1);
}
/* 鼠标在 0.1s 时移走:
正向走了 0.1s / 0.4s = 25% 的路程
反向回到 scale(1) 只需 0.1s,不是完整的 0.4s */如果浏览器不做任何优化,回程也用 0.4 秒,那只走了 25% 距离的回程就会显得特别慢,像是慢动作。规范定义的反向快捷机制(reversing shortening)让回程时长和正向已走时长保持比例关系。这样无论用户在什么时候移走鼠标,来回的速度感都是一致的。
连续快速切换时不会叠加
同一个属性在同一个元素上只能有一个活跃过渡。连续快速 hover 进出十次,不会产生十个排队的过渡。每次切换都是取消当前过渡、从当前位置发起新过渡。最终效果是属性值在两个端点之间平滑地来回摆动,不会出现抖动或跳帧。
但如果过渡时长设得很短,比如 50 毫秒,快速来回切换时每次中断位置都和端点很接近,视觉上就像在两个值之间弹跳。这不是浏览器 bug,而是时长太短导致插值区间太小,来不及看到平滑的过渡。遇到这种情况可以适当加长时长,或者给触发条件加防抖。
JS 直接改样式时的注意点
在 JS 里连续写两行改样式,比如先设 opacity: 0 再设 opacity: 1,浏览器会把它们合并到同一帧处理,只看到最终值 1,不会触发过渡。如果需要分两步触发,要在中间强制让浏览器做一次样式计算,常见做法是读一下 getComputedStyle(el).opacity 或访问 el.offsetHeight。这样浏览器会把第一次改动落实,第二次改动才能被识别为「值变了」从而启动过渡。
容易答错的地方
- 以为中断后从初始值重新开始
- 不会。过渡中断后新过渡的起点是中断瞬间的插值位置,不是 CSS 里写的初始值。如果正向走了 80%,反向就从 80% 的位置开始往回走。
- 以为反向过渡会用完整的原始时长
- 规范定义了反向快捷:如果正向只走了 25% 就被中断反向,回程时长是原始时长的 25%。直接用完整时长的话,回程会显得异常缓慢。
面试官还会怎么问?
两个不同属性同时被中断,时长缩短比例一样吗?
不一定。每个属性的过渡独立计算。如果 opacity 设了 0.2 秒、transform 设了 0.4 秒,中断时各自已播放的比例不同,缩短后的回程时长也不同。
animation 被打断是同样的行为吗?
不是。@keyframes 动画被打断后的行为取决于 animation-fill-mode 和 animation-play-state。暂停动画用 paused,移除动画会直接跳到目标状态。动画没有 transition 那种自动反向快捷的机制。
过渡中断时 transitionend 还会触发吗?
不会。过渡被中断时触发的是 transitioncancel 事件。代码里如果依赖 transitionend 来切换状态,中断场景下就会漏掉,需要同时监听 transitioncancel。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。