motion-anything 动效启动正常、播到一半停住,通常不是单一原因,而是两件事里至少中了一件:播放时长的取值本身偏短,或者清理与销毁调用在播放还没结束时就被触发了。判断顺序建议先看停止画面落在时间轴的哪个位置,再决定去查时长配置还是查调用时机,不要一上来就改时长。
动效中途停止,可以先按停止位置的画面特征分两类:停在动画定义的末态、不再变化,倾向时长或进度上限问题;停在中途、画面突然静止或被重置,倾向卸载、重复初始化打断了播放。两条线索分开验证,先固定时长看是否还停,再临时注释清理调用看是否还停。结论只在当前测试环境成立,换容器尺寸、换路由或换组件生命周期后需要复测。
确认停止位置是时长终点还是中途被打断
先把“停”这件事落到时间轴上,再谈原因。给动效的起点和终点各打一个时间戳,中间不停留,只记录两个时刻,就能判断停止点是否落在预期终点附近。
// 在触发播放的那一行记录起点
const t0 = performance.now();
console.log('[MOTION] start', t0);
// 在动效的结束回调里记录终点(回调名按你项目实际 API 替换)
onMotionEnd(() => {
console.log('[MOTION] end', performance.now() - t0, 'ms');
});
然后对照画面特征:
- 停在动画定义的最后一帧或最终样式,元素位置、透明度停在终态不动 —— 更像时长到点,播放确实走完了,只是走得太短,看起来像“中停”。
- 停在中途任意一帧,元素卡在半透明或半位移状态,随后可能被重置回初始样式 —— 更像播放被外部调用打断。
- 元素直接消失、被替换成另一个 DOM 或另一套样式 —— 优先怀疑所在组件被卸载,而不是动效本身。
两类情况在时间轴上的差别是:时长到点时,你记录的 end 时间戳会接近配置的时长;被打断时,往往根本收不到 end 回调,或者 end 的时间和配置时长对不上。先确认这一点,后面两条线索才有意义。
检查时长相关设置是写死还是按容器计算
时长设置的常见位置有几处,需要逐个确认,不要只看最外层那一处:动画配置对象里的 duration 或 durationInFrames 字段、CSS 里的 animation-duration / transition-duration、按容器宽度换算时长的计算函数、以及用逐帧驱动时自己写的总帧数。
写死数值和按尺寸计算,行为差别主要体现在容器变化之后:
- 写死数值:例如 duration 固定为 300 或 600,容器变宽、内容变多时,动效走完的距离变长而时间不变,观感上就像被“截断”。
- 按尺寸计算:例如按容器宽度除以一个速度常量得出时长,或者按元素数量乘以单段时长。这种写法在容器尺寸变化后仍能走完,但除数、速度常量取偏大时,时长同样会偏短。
排查步骤:先找到实际生效的那一处赋值,打印出来看数值;再把时长临时放大到原来的两到三倍,重新触发播放。如果这时能完整播完,问题就在时长取值;如果仍然停在同一个位置,说明时长不是主因,继续查清理调用。改完记得把临时值还原,不要把它当成修复方案留在代码里。
排查清理与销毁调用是否早于播放结束
清理调用通常出现在三类时机,建议按这个顺序查:
- 路由切换:页面离开时是否统一调用了销毁或停止方法,而当前动效还没播完。
- 组件卸载:unmount、onDestroy、disconnectedCallback 这类生命周期里是否无条件调用了 stop 或 dispose。
- 重复初始化:同一次进入里是否创建了两次实例,后一次初始化把前一次实例销毁,表现为播到一半停住。
用控制台标记调用的先后顺序,是最直接的确认方式。给创建和销毁各加一行输出,并打印调用栈,就能看出是谁先谁后、由谁触发:
function playMotion(el, options) {
console.log('[MOTION] init', new Error().stack);
// ... 创建实例
}
function disposeMotion(instance) {
console.log('[MOTION] dispose', new Error().stack);
// ... 释放资源
}
观察日志顺序:如果 dispose 出现在 start 之后、end 之前,基本可以确认是清理时机太早。此时把销毁调用改成等待播放结束再执行,或者加一个“播放中不销毁”的判断,再看是否还会中停。改动只作为定位手段,最终方案要结合组件的生命周期由你自己决定。
用日志或标记点记录播放进度
只记起点和终点还不够,中停的动效往往收不到终点回调。可以在播放过程中按固定间隔输出进度标记,让停止时刻有客观依据。
const t0 = performance.now();
const timer = setInterval(() => {
console.log('[MOTION] progress',
Math.round(performance.now() - t0), 'ms');
}, 100);
onFinish(() => {
clearInterval(timer);
console.log('[MOTION] finish',
Math.round(performance.now() - t0), 'ms');
});
标记间隔建议取 100 毫秒左右:太密会淹没控制台,太疏看不出停止点。如果动效总时长本身很短,可以改成按帧记录,只输出帧序号。做完之后,把画面停住的那一刻和控制台最后一条 progress 对齐,就能拿到停止时刻的近似值。若最后一条 progress 的时间明显小于配置时长,倾向被打断;若接近配置时长,倾向时长本身偏短。
写出两类原因的区分结论
把上面几步的记录整理成一段可复查的判断,而不是凭印象下结论。可以按这个模板写:
- 现象:动效在约多少毫秒处停止,停止画面停在终态还是中途,是否收到结束回调。
- 排除项:把时长放大后现象是否改变,把销毁调用注释后现象是否改变,两次对照分别排除了哪一类原因。
- 确认项:最终判定为时长取值偏短,或清理与销毁调用早于播放结束,二者取其一。
- 依据:控制台 start、progress、dispose、finish 各条日志的时间戳与顺序。
这样写出来的结论可以复核:换个人拿着同一份日志,也能得到同样的判断。需要提醒的是,上述判断只适用于当前测试环境,换容器尺寸、换路由或调整组件生命周期之后,停止位置和日志顺序都可能变化,需要重新跑一遍对照再下结论。