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

如何实现支持多元素操作的原生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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.27 11:57:19