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

currentPage的animationend事件重复触发Bug求助

问题:animationend事件重复触发的排查与解决

问题描述

点击箭头会调用updateArrows函数,最终执行next()更新步骤。向下切换到步骤4时一切正常,但从步骤4向上切回步骤2时,currentPage.addEventListener("animationend",function (e) { })会触发两次,控制台先后输出正确的步骤2和旧的步骤4。推测是事件重复绑定,但无法理解为何前期无此问题。

截图

步骤切换正常时的控制台输出
步骤回退时的控制台输出

相关代码

const changeStep = document.querySelectorAll(".step");
const currentPaginate = document.querySelector(".pagination span.active");
const arrows = document.querySelectorAll(".arrow");
let isAnimating = false;

for (let arrow of arrows) arrow.addEventListener("click", updateArrows);

for (let step of changeStep) step.addEventListener("click", next);

function updateArrows(e, currentStep = null) {
    if (isAnimating) return false;
    let arrow = null;
    e.target ? (arrow = e.target) : (arrow = e);
    let nextStep;
    if (currentStep == null) {
        currentStep = document.querySelector(".step.current");
        if (arrow.classList.contains("arrow-bottom")) {
            nextStep = currentStep.nextElementSibling;
        } else {
            nextStep = currentStep.previousElementSibling;
        }
    } else {
        nextStep = document.querySelector(".step.current");
    }
    if (!arrow.classList.contains("impossible")) {
        if (nextStep.dataset.id != 1 && nextStep.dataset.id != 4) {
            arrows.forEach(function (arrow) {
                if (arrow.classList.contains("impossible")) {
                    arrow.classList.remove("impossible");
                }
            });
        } else if (nextStep.dataset.id == 4) {
            if (arrow.previousElementSibling.classList.contains("impossible"))
                arrow.previousElementSibling.classList.remove("impossible");
            arrow.classList.add("impossible");
        } else if (nextStep.dataset.id == 1) {
            if (arrow.nextElementSibling.classList.contains("impossible"))
                arrow.nextElementSibling.classList.remove("impossible");
            arrow.classList.add("impossible");
        }
        if (e.target != undefined) next(nextStep);
    }
}

function next(e) {
    if (isAnimating) {
        return false;
    }
    isAnimating = true;
    let step = null;
    const currentPage = document.querySelector(".page.current");
    e.target ? (step = e.target) : (step = e);

    if (!step.classList.contains("current")) {
        const currentStep = document.querySelector(".step.current");
        const nextStep = step.dataset.id;
        const nextPage = document.querySelector('[data-id="' + nextStep + '"]');
        currentStep.classList.remove("current");
        step.classList.add("current");
        currentPaginate.textContent = "0" + nextStep;
        let arrow;
        if (currentStep.dataset.id < nextStep) {
            arrow = document.querySelector(".arrow-bottom");
        } else {
            arrow = document.querySelector(".arrow-top");
        }
        currentPage.addEventListener(
            "animationend",
            function (e) {
                e = window.event || e;
                if (this === e.target) {
                    currentPage.classList.remove("current", "moveToBottomFade");
                    nextPage.classList.add("current");
                    console.log(nextPage);
                    isAnimating = false;
                }
            },
            true
        );
        currentPage.classList.add("moveToBottomFade");
    }
}

问题根源

每次调用next()时,都会给当前的currentPage绑定一次animationend事件,但从未移除已绑定的监听:

  • 向下切换时,每次绑定事件的是不同的currentPage(比如从1→2绑定page1,2→3绑定page2),旧页面的事件监听不会干扰新切换,所以前期无问题;
  • 回退时,比如从4→3给page4绑定了事件,3→2给page3绑定事件,此时page4的监听仍存在。后续如果page4的动画相关状态变化触发了animationend(或因捕获模式true导致事件冒泡时被触发),就会执行旧的监听逻辑,导致重复输出。

解决方法

方法1:使用once自动移除监听(推荐)

修改addEventListener的第三个参数为配置对象,设置once: true,事件触发一次后会自动移除监听:

currentPage.addEventListener(
    "animationend",
    function (e) {
        e = window.event || e;
        if (this === e.target) {
            currentPage.classList.remove("current", "moveToBottomFade");
            nextPage.classList.add("current");
            console.log(nextPage);
            isAnimating = false;
        }
    },
    { capture: true, once: true } // 替换原布尔值为配置对象
);

方法2:命名函数+手动移除监听

将匿名事件处理函数改为命名函数,触发后手动移除监听:

function handlePageAnimationEnd(e) {
    e = window.event || e;
    if (this === e.target) {
        currentPage.classList.remove("current", "moveToBottomFade");
        nextPage.classList.add("current");
        console.log(nextPage);
        isAnimating = false;
        // 移除当前元素的事件监听
        currentPage.removeEventListener("animationend", handlePageAnimationEnd, true);
    }
}

// 绑定事件
currentPage.addEventListener("animationend", handlePageAnimationEnd, true);

方法3:绑定前清理旧监听

如果需要保留多次触发的可能,可在绑定新监听前先移除旧的:

// 先移除可能存在的旧监听
currentPage.removeEventListener("animationend", handlePageAnimationEnd, true);
// 再绑定新监听
currentPage.addEventListener("animationend", handlePageAnimationEnd, true);

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.21 17:36:10