如何实现支持多元素操作的原生JS版slideToggle/slideUp方法
原生JavaScript实现批量元素滑动显隐方案
jQuery的滑动动画核心逻辑是通过动态过渡元素的height、上下内边距、上下外边距值,配合overflow: hidden裁切内容实现平滑显隐,下面的实现完全兼容单元素、多集合批量操作,效果和jQuery原生方法一致,无额外依赖。
首先修正你代码里的元素获取问题:getElementsByClassName不支持传入多个类名参数,多类场景统一用querySelectorAll即可,传入标准CSS选择器(多选择器用逗号分隔),方法内部已经做了入参兼容,支持直接传DOM元素、NodeList集合、HTMLCollection集合,甚至直接传选择器字符串。
完整实现代码
// 入参兼容工具:统一把各种格式的目标输入转为DOM元素数组 const normalizeElements = (targets) => { if (targets instanceof Element) return [targets]; if (targets instanceof NodeList || targets instanceof HTMLCollection) return Array.from(targets); if (typeof targets === 'string') return Array.from(document.querySelectorAll(targets)); return []; }; const slideUp = (targets, duration = 400) => { const elements = normalizeElements(targets); elements.forEach(el => { const computedStyle = window.getComputedStyle(el); // 已隐藏元素直接跳过,避免重复触发 if (computedStyle.display === 'none') return; // 清除正在运行的动画,防止快速触发导致动画堆叠错乱 cancelAnimationFrame(el._slideAnimId); el.style.overflow = 'hidden'; // 记录动画起始样式值 const startHeight = el.offsetHeight; const startPaddingTop = parseFloat(computedStyle.paddingTop); const startPaddingBottom = parseFloat(computedStyle.paddingBottom); const startMarginTop = parseFloat(computedStyle.marginTop); const startMarginBottom = parseFloat(computedStyle.marginBottom); const startTime = performance.now(); // 逐帧动画 const runFrame = () => { const elapsed = performance.now() - startTime; const progress = Math.min(elapsed / duration, 1); // 采用和jQuery默认swing一致的缓动曲线 const eased = 0.5 - Math.cos(progress * Math.PI) / 2; el.style.height = `${startHeight * (1 - eased)}px`; el.style.paddingTop = `${startPaddingTop * (1 - eased)}px`; el.style.paddingBottom = `${startPaddingBottom * (1 - eased)}px`; el.style.marginTop = `${startMarginTop * (1 - eased)}px`; el.style.marginBottom = `${startMarginBottom * (1 - eased)}px`; if (progress < 1) { el._slideAnimId = requestAnimationFrame(runFrame); } else { // 动画结束清理临时内联样式,还原布局控制 el.style.display = 'none'; ['height', 'paddingTop', 'paddingBottom', 'marginTop', 'marginBottom', 'overflow'].forEach(prop => { el.style[prop] = ''; }); } }; requestAnimationFrame(runFrame); }); }; const slideDown = (targets, duration = 400) => { const elements = normalizeElements(targets); elements.forEach(el => { const computedStyle = window.getComputedStyle(el); // 已显示元素直接跳过 if (computedStyle.display !== 'none') return; cancelAnimationFrame(el._slideAnimId); // 临时显示元素获取真实目标高度 el.style.display = 'block'; el.style.overflow = 'hidden'; const endHeight = el.offsetHeight; const endPaddingTop = parseFloat(computedStyle.paddingTop); const endPaddingBottom = parseFloat(computedStyle.paddingBottom); const endMarginTop = parseFloat(computedStyle.marginTop); const endMarginBottom = parseFloat(computedStyle.marginBottom); // 重置为动画初始状态 el.style.height = '0px'; el.style.paddingTop = '0px'; el.style.paddingBottom = '0px'; el.style.marginTop = '0px'; el.style.marginBottom = '0px'; // 强制重绘保证初始状态生效 el.offsetHeight; const startTime = performance.now(); const runFrame = () => { const elapsed = performance.now() - startTime; const progress = Math.min(elapsed / duration, 1); const eased = 0.5 - Math.cos(progress * Math.PI) / 2; el.style.height = `${endHeight * eased}px`; el.style.paddingTop = `${endPaddingTop * eased}px`; el.style.paddingBottom = `${endPaddingBottom * eased}px`; el.style.marginTop = `${endMarginTop * eased}px`; el.style.marginBottom = `${endMarginBottom * eased}px`; if (progress < 1) { el._slideAnimId = requestAnimationFrame(runFrame); } else { // 清理临时样式 ['height', 'paddingTop', 'paddingBottom', 'marginTop', 'marginBottom', 'overflow'].forEach(prop => { el.style[prop] = ''; }); } }; requestAnimationFrame(runFrame); }); }; const slideToggle = (targets, duration = 400) => { const elements = normalizeElements(targets); elements.forEach(el => { if (window.getComputedStyle(el).display === 'none') { slideDown(el, duration); } else { slideUp(el, duration); } }); };
使用方法
批量操作元素
querySelectorAll传入多选择器时需要给每个类名加.前缀,用逗号分隔:
// 获取多个目标元素 const targetList = document.querySelectorAll(".class1, .class2, .class3"); // 批量执行上滑隐藏,动画时长500ms slideUp(targetList, 500); // 也可以直接传选择器字符串,省略手动获取元素的步骤 slideToggle(".nav", 400);
替换原有jQuery事件逻辑
你之前写的菜单点击切换导航的代码,可以直接替换为下面的原生写法:
const $context = getContext(context); $context.addEventListener('click', (e) => { if (e.target.matches('.menu')) { slideToggle('.nav'); } });
特性说明
- 默认动画时长400ms,和jQuery默认值保持一致,不传时长参数时自动使用默认值
- 内置动画队列清理逻辑,快速重复触发不会出现高度错乱、动画堆叠问题
- 动画结束后自动清除临时添加的内联样式,不会覆盖原有CSS的布局规则
- 缓动曲线和jQuery默认的swing效果完全一致,视觉体验无差异
内容的提问来源于stack exchange,提问作者proto
相关产品推荐
相关产品推荐

