
本文详解 vanilla JS 中 sidebar 展开/收起动画中因重复绑定 transitionend 事件导致的 class 叠加问题,并提供可复用、健壮的修复方案。
本文详解 vanilla js 中 sidebar 展开/收起动画中因重复绑定 `transitionend` 事件导致的 class 叠加问题,并提供可复用、健壮的修复方案。
在实现带延迟逐项展开/收起的侧边栏(.sidebar)时,一个常见却隐蔽的陷阱是:多次点击触发后,.sidebar-element 的 active 类被重复添加,或 .sidebar 的 active 类无法及时清除。根本原因并非 classList.add()/remove() 本身不幂等(它们确实是安全的),而在于 transitionend 事件监听器被反复注册却未清理,造成「一次过渡结束,触发多次回调」,进而引发状态错乱与视觉异常。
核心问题出现在原代码的收起逻辑中:
if (key === 0) {
contents[0].addEventListener('transitionend', () => {
sidebar.classList.remove('active');
});
}
每次点击「收起」按钮,都会为 contents[0] 新增一个 transitionend 监听器。当元素完成收起动画时,所有已注册的监听器将依次执行,导致 sidebar.classList.remove('active') 被调用多次——虽无副作用,但更严重的是:若用户快速连续点击,前序未清理的监听器可能在后续动画中意外触发(例如展开时的 transitionend 也被捕获),破坏状态同步。
✅ 正确解法是:使用具名函数并确保监听器仅注册一次、执行后立即移除。优化后的关键逻辑如下:
const SIDEBAR_DELAY_TIME = 150;
function handleSidebar() {
const btn = document.querySelector('button');
const sidebar = document.querySelector('.sidebar');
const contents = document.querySelectorAll('.sidebar-element');
// ✅ 提前定义可移除的 transitionend 回调
const onFirstElementTransitionEnd = () => {
sidebar.classList.remove('active');
contents[0].removeEventListener('transitionend', onFirstElementTransitionEnd);
};
if (!btn || !sidebar) return;
btn.addEventListener('click', () => {
if (!sidebar.classList.contains('active')) {
// 展开:激活 sidebar,再逐个激活内容项
sidebar.classList.add('active');
contents.forEach((item, i) => {
setTimeout(() => item.classList.add('active'), i * SIDEBAR_DELAY_TIME);
});
} else {
// 收起:反向逐个移除 active 类,并为首个元素绑定一次性 transitionend
const indices = Array.from({ length: contents.length }, (_, i) => i).reverse();
indices.forEach((idx, i) => {
setTimeout(() => contents[idx].classList.remove('active'), i * SIDEBAR_DELAY_TIME);
// ✅ 仅对最后一个要收起的元素(即原数组第一个,索引 0)绑定监听器
if (idx === 0) {
contents[0].addEventListener('transitionend', onFirstElementTransitionEnd);
}
});
}
});
}
handleSidebar();
? 关键注意事项:
-
transitionend事件需绑定在实际发生 CSS 过渡的元素上(此处是contents[0]),且必须确保该元素在收起动画中确实触发了过渡(检查其opacity和transform是否设置了transition)。 - 避免在循环内直接使用匿名箭头函数注册事件监听器,否则无法精准移除。
- 若存在多个过渡属性(如
opacity和transform),transitionend可能触发多次。如需严格控制,可结合event.propertyName过滤(例如只响应transform):const onFirstElementTransitionEnd = (e) => { if (e.propertyName === 'transform') { sidebar.classList.remove('active'); contents[0].removeEventListener('transitionend', onFirstElementTransitionEnd); } };
此方案保证了事件监听的精确性与资源的可控性,使 sidebar 动画在任意频率点击下均保持状态一致、行为可预测,是 vanilla JS 动画交互中的最佳实践之一。










