You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

JavaScript动画队列类问题:后续动画失效且无法连续执行

DOM动画队列类问题修复

问题描述

我封装了一个用于DOM元素动画的JavaScript类AnimateElement,目的是实现动画的顺序执行,但遇到两个核心问题:

  • 第一个动画执行完毕后,后续动画没有过渡效果,直接跳转到终点位置;
  • 第二个动画之后的所有后续动画都无法执行。

查看控制台日志发现,animationend事件处理函数仅被调用一次——虽然处理函数内会移除自身的事件监听,但move方法会重新添加该监听,逻辑上看似没问题但实际不生效。

原代码如下:

export class AnimateElement {
    constructor(el) {
        this.el = el;
        this.animationQueue = [];
        this.handleAnimationEnd = this.handleAnimationEnd.bind(this);
    }

    addAnimation(fromx, fromy, tox, toy, delay, time, func = 'linear', direction = 'forwards', handler = null) {
        this.animationQueue.push({ fromx, fromy, tox, toy, delay, time, func, direction, handler });
console.log(this.animationQueue);
    }

    move() {
console.log('move called.', this.animationQueue.length, this.animationQueue);
        if (this.animationQueue.length > 0) {
            let oper = this.animationQueue.shift();

            let fromx = oper.fromx !== null ? oper.fromx : parseFloat(this.el.style.getPropertyValue('--translate-to-x')) || 0;
            let fromy = oper.fromy !== null ? oper.fromy : parseFloat(this.el.style.getPropertyValue('--translate-to-y')) || 0;

console.log('fromx:', fromx, 'fromy:', fromy);

            this.el.style.setProperty('--translate-from-x', fromx + 'px');
            this.el.style.setProperty('--translate-from-y', fromy + 'px');
            this.el.style.setProperty('--translate-to-x', oper.tox + 'px');
            this.el.style.setProperty('--translate-to-y', oper.toy + 'px');
            this.el.style.setProperty('--animate-time', oper.time + 'ms');
            this.el.style.setProperty('--animate-func', oper.func);
            this.el.style.setProperty('--animate-delay', oper.delay + 'ms');
            this.el.style.setProperty('--animate-direction', oper.direction);
            this.el.currentAnimationHandler = oper.handler;
            this.el.addEventListener('animationend', this.handleAnimationEnd);
console.log('added listener.');
            this.el.classList.add('animate-transform');
        }
    }

    handleAnimationEnd(event) {
console.log('handleAnimationEnd called.');
        // Update the from coordinates to be the to coordinates for the next animation
        this.el.style.setProperty('--translate-from-x', this.el.style.getPropertyValue('--translate-to-x'));
        this.el.style.setProperty('--translate-from-y', this.el.style.getPropertyValue('--translate-to-y'));

        event.target.classList.remove('animate-transform');
        event.target.removeEventListener('animationend', this.handleAnimationEnd);
console.log('removed listener.');
        if (this.el.currentAnimationHandler) {
            this.el.currentAnimationHandler();
        }
        this.move(); // Trigger the next animation in the queue
    }
}

问题根源

  1. 无过渡跳转:移除animate-transform类后立即添加该类,浏览器会合并这两次样式变更,不会重新触发动画,导致直接跳转到终点。
  2. 后续动画失效:animationend事件仅触发一次,是因为样式合并导致第二次动画根本没真正执行,自然不会触发事件;同时同步调用move没有给浏览器足够时间处理样式更新,导致后续动画的事件监听无法正常工作。

修复后的代码

export class AnimateElement {
    constructor(el) {
        this.el = el;
        this.animationQueue = [];
        this.handleAnimationEnd = this.handleAnimationEnd.bind(this);
    }

    addAnimation(fromx, fromy, tox, toy, delay, time, func = 'linear', direction = 'forwards', handler = null) {
        this.animationQueue.push({ fromx, fromy, tox, toy, delay, time, func, direction, handler });
    }

    move() {
        if (this.animationQueue.length === 0) return;
        
        const oper = this.animationQueue.shift();

        // 获取当前元素的最终位置作为下一个动画的起点
        const currentToX = parseFloat(this.el.style.getPropertyValue('--translate-to-x')) || 0;
        const currentToY = parseFloat(this.el.style.getPropertyValue('--translate-to-y')) || 0;
        const fromx = oper.fromx !== null ? oper.fromx : currentToX;
        const fromy = oper.fromy !== null ? oper.fromy : currentToY;

        // 更新动画变量
        this.el.style.setProperty('--translate-from-x', `${fromx}px`);
        this.el.style.setProperty('--translate-from-y', `${fromy}px`);
        this.el.style.setProperty('--translate-to-x', `${oper.tox}px`);
        this.el.style.setProperty('--translate-to-y', `${oper.toy}px`);
        this.el.style.setProperty('--animate-time', `${oper.time}ms`);
        this.el.style.setProperty('--animate-func', oper.func);
        this.el.style.setProperty('--animate-delay', `${oper.delay}ms`);
        this.el.style.setProperty('--animate-direction', oper.direction);
        this.el.currentAnimationHandler = oper.handler;

        // 避免重复绑定事件,先移除再添加
        this.el.removeEventListener('animationend', this.handleAnimationEnd);
        this.el.addEventListener('animationend', this.handleAnimationEnd);

        // 强制浏览器重绘,确保样式变更被识别
        void this.el.offsetWidth;
        this.el.classList.add('animate-transform');
    }

    handleAnimationEnd(event) {
        // 移除动画类
        event.target.classList.remove('animate-transform');
        event.target.removeEventListener('animationend', this.handleAnimationEnd);

        // 执行当前动画的回调
        if (this.el.currentAnimationHandler) {
            this.el.currentAnimationHandler();
            this.el.currentAnimationHandler = null;
        }

        // 延迟触发下一个动画,确保样式更新完成
        requestAnimationFrame(() => {
            this.move();
        });
    }
}

修复要点

  • 强制重绘:通过void this.el.offsetWidth触发浏览器重绘,避免样式合并,让动画类的添加能正确触发过渡效果。
  • 延迟启动下一个动画:使用requestAnimationFrame确保当前动画的样式清理完成后再启动下一个动画,保证animationend事件能正常触发。
  • 事件监听去重:添加新监听前先移除一次,防止重复绑定导致的逻辑混乱。

内容的提问来源于stack exchange,提问作者omega royal

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.21 11:00:57