HTML dialog元素关闭时.getAnimations()返回的Promise为何会被拒绝?
嘿,这个问题我之前做dialog过渡动画的时候也踩过类似的坑,咱们来唠唠这背后的原因和解决办法~
首先,你遇到的Promise被拒绝的情况,大概率是因为动画被浏览器强制中止了,而不是正常播放完成。
为什么会被中止?
当dialog的close事件触发时,浏览器其实已经开始改变它的状态了:open属性被移除,元素的堆叠上下文(尤其是你用了Chrome独有的overlay过渡属性)也在快速调整。这时候浏览器可能会直接终止和dialog(包括它的backdrop)关联的部分动画,导致animation.finished这个Promise抛出AbortError而拒绝。
加上你用了Promise.any——这个方法的特性是只要有一个Promise被拒绝,它就会立即跟着拒绝,所以哪怕只有一个backdrop动画被中止,整个逻辑就会“翻车”。
该怎么解决?
给你几个实用的调整方案:
1. 换用更稳妥的Promise处理方式
别再用Promise.any了,换成Promise.allSettled或者给每个动画的finishedPromise加错误捕获:
$dialog.addEventListener('close', async function() { // 别忘了同时获取dialog本身和backdrop的动画! const allAnimations = [ ...$dialog.getAnimations(), ...$dialog.backdropElement.getAnimations() ]; if (allAnimations.length) { // 方案一:用allSettled,等所有动画要么完成要么中止 await Promise.allSettled(allAnimations.map(anim => anim.finished)); // 方案二:给每个Promise加catch,忽略中止错误 // await Promise.all(allAnimations.map(anim => anim.finished.catch(() => {}))); console.log('After close'); } });
这样不管个别动画有没有被中止,我们都能等所有动画相关的操作结束后再执行后续逻辑。
2. 换个时机触发关闭逻辑(更推荐)
与其在close事件里被动等动画,不如主动控制退出流程:先触发过渡动画,等动画真的完成后再调用dialog.close()。
调整JS代码:
const $button = document.querySelector('button'); const $dialog = document.querySelector('dialog'); $button.addEventListener('click', function() { if ($dialog.open) { // 先给dialog加一个关闭状态的类 $dialog.classList.add('closing'); // 监听动画结束事件,完成后再真正关闭dialog const handleAnimEnd = () => { $dialog.close(); $dialog.classList.remove('closing'); $dialog.removeEventListener('transitionend', handleAnimEnd); }; // 用once: true确保只触发一次 $dialog.addEventListener('transitionend', handleAnimEnd, { once: true }); } else { $dialog.showModal(); } });
对应的CSS也要调整,用类来控制退出过渡,避开overlay的兼容性问题:
.dialog { --_transition-duration: 200ms; border: 0; border-radius: .25rem; box-shadow: 0 0.0625rem .5rem 0 rgb(0, 0, 0, .1); opacity: 0; scale: .9; overscroll-behavior: contain; padding: 0; transition: opacity var(--_transition-duration) linear, scale var(--_transition-duration) linear; width: min(calc(100% - 2rem), 30rem); } .dialog[open] { opacity: 1; scale: 1; } /* 关闭时的过渡状态 */ .dialog.closing { opacity: 0; scale: .9; } .dialog::backdrop { background-color: rgb(0, 0, 0, .375); opacity: 0; transition: opacity var(--_transition-duration) linear; } .dialog[open]::backdrop { opacity: 1; } .dialog.closing::backdrop { opacity: 0; }
这种方式完全由我们控制动画和关闭的时机,浏览器不会中途强制中止动画,行为更稳定,还能兼容Safari和Firefox。
是正常行为还是浏览器bug?
更偏向于Chrome在处理overlay过渡和dialog关闭时机的边缘场景问题。毕竟overlay是比较新的属性,目前只有Chrome支持,浏览器在结合dialog的状态变化处理这类过渡时,逻辑还不够完善,导致动画被意外中止。
如果只在Chrome环境下开发,用第一种Promise的调整方式就能解决;如果要兼容多浏览器,第二种主动控制的方案会更可靠。
备注:内容来源于stack exchange,提问作者R. Groot Jebbink

