最可控方式是封装 animationend 事件为 promise,每个元素单独监听并设 once: true,添加超时兜底;链式调用需每步返回 promise,避免上下文丢失和动画取消导致的 promise 悬停。

Promise 链式调用如何接住 animationend 事件
直接监听 animationend 并返回 Promise 是最可控的方式,避免依赖 CSS 动画时长硬编码。浏览器原生事件不返回 Promise,必须手动包装。
- 每个动画元素需单独监听自己的
animationend,不能靠父容器统一监听(事件不会冒泡到非动画元素) - 务必添加
once: true选项,否则重复触发或内存泄漏 - 需处理动画被提前取消的情况(如元素被移除、
animation-name变为none),此时animationend不会触发,应加超时兜底
function waitForAnimation(el) {
return new Promise(resolve => {
const timeout = setTimeout(() => resolve(), 1000);
el.addEventListener('animationend', () => {
clearTimeout(timeout);
resolve();
}, { once: true });
});
}
多个 DOM 元素动画如何串成 Promise 链
链式调用本质是 then() 返回新 Promise,所以每个环节必须返回 Promise,不能只写 el.animate(...) 这类无返回值操作。
-
el.animate()返回的是Animation实例,不是 Promise,不能直接.then() - 若使用 Web Animations API,可结合
finished属性(它是个 Promise),但注意兼容性:Safari 直到 17.4 才支持finished的正确 resolve 行为 - 更稳妥的做法仍是封装
animationend,兼容所有现代浏览器
el1.classList.add('fade-in');
waitForAnimation(el1)
.then(() => {
el2.classList.add('slide-up');
return waitForAnimation(el2);
})
.then(() => {
el3.classList.add('rotate');
return waitForAnimation(el3);
});
如何避免链式调用中 this 或上下文丢失
在箭头函数或 then() 回调里访问 DOM 元素时,容易因作用域或元素已被移除导致 Cannot read property 'addEventListener' of null。
- 不要在链中反复查询
document.querySelector,应在链外提前获取并保持引用 - 如果动画过程中元素可能被销毁(如 Vue/React 组件卸载),需在 Promise 回调里检查
el.isConnected - 避免在
then()里修改已进入动画的元素 class 后立刻再监听——要等 class 生效(微任务后),否则animationend可能漏掉
用 async/await 写更清晰,但要注意错误传播
async/await 是 Promise 链的语法糖,可读性更好,但未捕获的异常会让整个链中断,后续动画完全不执行。
- 每个
await waitForAnimation(el)前建议加 try/catch,尤其当动画依赖网络加载或条件判断时 - 不要在
catch里吞掉错误却不重置状态(比如没清除已添加的 class),否则界面可能卡在中间态 - 若某步失败后仍想继续后续动画,可用
await waitForAnimation(el).catch(() => {})显式忽略
animationend 永远不触发,Promise 悬而未决。超时时间不能设太短(否则干扰正常慢动画),也不能设太长(影响交互反馈),1000ms 是个较平衡的默认值,但关键路径建议根据具体动画时长动态计算。










