基于JavaScript经典实例的动画效果实现代码

作者:巡夜人

时间:2026-10-30

浏览:79

本文围绕基于JavaScript经典实例的动画效果实现代码展开,详细解析requestAnimationFrame核心机制、缓动插值算法,并提供淡入淡出、物理弹跳、高度折叠等典型实例代码与性能优化方案。

文章目录

实现网页动态交互的核心,在于使用代码精准控制元素在时间轴上的状态演变。本文通过剖析现代浏览器渲染节拍,围绕元素淡入淡出、物理阻尼弹跳以及折叠面板展开收起三个经典实例,提供完整、可直接复用的原生 JavaScript 动画实现代码,并深入探讨帧驱动机制与性能优化原则。

一、JavaScript 动画驱动核心机制:从定时器到 requestAnimationFrame

早期的 Web 动画主要依赖 setInterval 或 setTimeout,通过每隔固定毫秒数(例如 16ms 或 20ms)修改 DOM 元素的样式属性。然而,定时器仅属于 JavaScript 引擎的任务队列,其回调触发时间受宏任务阻塞影响,无法与显示设备的硬件刷新率(通常为 60Hz,即约 16.67ms 一帧)保持对齐。当定时器回调在两次屏幕绘制中间执行或连续挤压执行时,就会出现肉眼可见的卡顿、掉帧以及画面撕裂。

现代浏览器推荐的标准解决方案是使用 window.requestAnimationFrame(简称 rAF)。它让浏览器掌握动画的主动权,在下一次重绘前统筹调用传入的回调函数,并自动注入高精度时间戳参数。当页面处于后台标签页或最小化时,浏览器还会智能暂停调用,从而大幅节省 CPU 与电池能耗。

基于JavaScript经典实例的动画效果实现代码
图 1:传统定时器与 requestAnimationFrame 调度机制对比

二、标准动画循环执行流程与通用骨架

编写稳健的 JavaScript 动画,绝不能使用“每帧固定增加固定像素”的累加方式,因为如果某几帧发生微小卡顿,累加法会导致动画整体被拉长,实际运动耗时失真。

正确的做法是“时间驱动插值”:预先设定动画的总持续时间(如 500ms),在每一帧中获取当前时间戳,计算出当前的完成比例(0 到 1 之间),再结合缓动函数(Easing Function)映射得到目标数值。

基于JavaScript经典实例的动画效果实现代码
图 2:requestAnimationFrame 标准动画执行步骤与闭环控制

三、经典实例一:平滑淡入淡出(透明度渐变动画)

淡入淡出是界面展示与隐藏最常用的过渡效果。该效果直接操作 opacity 属性,因为 opacity 可以由浏览器合成器线程直接处理,避免引发繁重的重新布局(Reflow)。

在实现淡入淡出时,需要处理连续点击时的冲突问题。如果前一个淡入动画尚未结束,用户再次触发淡出,必须能够通过 cancelAnimationFrame 取消上一轮动画,并以当前实时透明度为起点平滑过渡,防止突变跳跃。

/**
 * 元素平滑淡入淡出封装
 * @param {HTMLElement} element - 目标DOM元素
 * @param {number} targetOpacity - 目标透明度 (0 ~ 1)
 * @param {number} duration - 动画持续时间 (毫秒)
 * @param {Function} [callback] - 动画完成后的回调
 */
function animateOpacity(element, targetOpacity, duration = 300, callback) {
  // 如果元素上已有运行中的动画句柄,立即取消以防重叠竞争
  if (element._animationFrameId) {
    cancelAnimationFrame(element._animationFrameId);
  }

  const computedStyle = window.getComputedStyle(element);
  const startOpacity = parseFloat(computedStyle.opacity) || 0;
  const deltaOpacity = targetOpacity - startOpacity;

  // 若数值已一致,无需执行动画
  if (Math.abs(deltaOpacity) < 0.001) {
    if (typeof callback === 'function') callback();
    return;
  }

  let startTime = null;

  function step(currentTime) {
    if (!startTime) startTime = currentTime;
    const elapsed = currentTime - startTime;
    const progress = Math.min(elapsed / duration, 1);

    // 使用二次方减速缓动公式 (Ease-out Quad)
    const eased = 1 - (1 - progress) * (1 - progress);
    const currentOpacity = startOpacity + deltaOpacity * eased;

    element.style.opacity = currentOpacity.toFixed(4);

    if (progress < 1) {
      element._animationFrameId = requestAnimationFrame(step);
    } else {
      element.style.opacity = targetOpacity;
      element._animationFrameId = null;
      if (typeof callback === 'function') {
        callback();
      }
    }
  }

  element._animationFrameId = requestAnimationFrame(step);
}

// 使用示例:淡入元素并在完成后输出日志
const box = document.getElementById('myBox');
animateOpacity(box, 1, 400, () => {
  console.log('淡入完成');
});

四、经典实例二:弹性物理与缓动曲线(弹跳小球动画)

现实世界的运动绝非匀速直线运动。要让界面生动自然,必须引入缓动函数或基于物理的加速度模拟。

以下示例展示一个模拟小球从高处下落并发生阻尼回弹的弹跳动画。通过数学插值函数模拟反弹衰减,能够清晰展示缓动函数如何将原本单调的线性时间转化成逼真的动力学效果。

基于JavaScript经典实例的动画效果实现代码
图 3:线性、减速缓动与物理弹跳曲线轨迹对比

下面是实现带回弹物理质感位移动画的完整代码:

/**
 * 经典 Bounce-Out 缓动公式
 * @param {number} t - 归一化时间进度 (0 ~ 1)
 * @returns {number} 归一化位移进度 (0 ~ 1)
 */
function easeOutBounce(t) {
  const n1 = 7.5625;
  const d1 = 2.75;

  if (t < 1 / d1) {
    return n1 * t * t;
  } else if (t < 2 / d1) {
    t -= 1.5 / d1;
    return n1 * t * t + 0.75;
  } else if (t < 2.5 / d1) {
    t -= 2.25 / d1;
    return n1 * t * t + 0.9375;
  } else {
    t -= 2.625 / d1;
    return n1 * t * t + 0.984375;
  }
}

/**
 * 弹跳位移动画实现
 * @param {HTMLElement} element - 运动元素
 * @param {number} targetY - 终点 Y 轴偏移量 (像素)
 * @param {number} duration - 持续时间
 */
function animateBounce(element, targetY = 300, duration = 1200) {
  const startY = 0;
  const distance = targetY - startY;
  let startTime = null;

  function loop(currentTime) {
    if (!startTime) startTime = currentTime;
    const elapsed = currentTime - startTime;
    const progress = Math.min(elapsed / duration, 1);

    const easedProgress = easeOutBounce(progress);
    const currentY = startY + distance * easedProgress;

    // 优先使用 transform 改变位移,避免触发重排
    element.style.transform = `translate3d(0, ${currentY.toFixed(2)}px, 0)`;

    if (progress < 1) {
      requestAnimationFrame(loop);
    }
  }

  requestAnimationFrame(loop);
}

五、经典实例三:手风琴式高度自适应折叠展开

CSS 的 transition 无法对 height: auto 进行直接过渡,这是前端开发中常见的问题。而纯 JavaScript 动画则能通过获取容器内容的真实高度(scrollHeight),将其作为目标数值进行连续插值,从而实现平滑自然的展开与收起。

实现展开与收起时,要特别注意容器的 overflow: hidden 设定以及完成时的状态清理:展开完成后应将行内高度清理回 auto,确保后续内部内容增加时高度能自适应撑开。

/**
 * 展开收起折叠面板动画
 * @param {HTMLElement} panel - 折叠容器元素
 * @param {boolean} isExpand - true为展开,false为收起
 * @param {number} duration - 动画时间
 */
function toggleAccordion(panel, isExpand, duration = 300) {
  panel.style.overflow = 'hidden';

  // 获取起点高度与目标高度
  const startHeight = panel.getBoundingClientRect().height;
  let targetHeight = 0;

  if (isExpand) {
    // 临时取消限制获取真实完整内容高度
    const prevHeight = panel.style.height;
    panel.style.height = 'auto';
    targetHeight = panel.scrollHeight;
    panel.style.height = prevHeight;
  } else {
    targetHeight = 0;
  }

  const heightDelta = targetHeight - startHeight;
  let startTime = null;

  function step(time) {
    if (!startTime) startTime = time;
    const elapsed = time - startTime;
    const progress = Math.min(elapsed / duration, 1);

    // Ease-in-out Quad 缓动
    const eased = progress < 0.5
      ? 2 * progress * progress
      : 1 - Math.pow(-2 * progress + 2, 2) / 2;

    const currentHeight = startHeight + heightDelta * eased;
    panel.style.height = `${currentHeight.toFixed(1)}px`;

    if (progress < 1) {
      requestAnimationFrame(step);
    } else {
      // 动画完成收尾
      if (isExpand) {
        panel.style.height = 'auto'; // 保持响应式
      } else {
        panel.style.height = '0px';
      }
    }
  }

  requestAnimationFrame(step);
}

六、JavaScript 动画性能优化与避坑要点

动画流畅度的根本是维持 60fps(每帧预算仅 16.6ms)。在编写 JavaScript 动画时,必须防范以下性能隐患:

  • 属性选择:优先使用 transform 和 opacity
    避免频繁改变 left、top、margin 或 width,这些几何属性会触发浏览器的重排(Layout)与重绘(Paint)。而 transform 和 opacity 仅触发合成层操作,可直接在 GPU 中完成。
  • 警惕强制同步布局(Layout Thrashing)
    在动画帧回调中,禁止“先写样式后读几何属性”的交替操作。例如连续在循环中调用 element.style.top = ... 然后又读取 element.offsetTop,会强迫浏览器提前重排计算,导致极度掉帧。
  • 及时取消未结束的动画帧句柄
    当元素被从 DOM 树移除或开启新动画时,务必调用 cancelAnimationFrame(handle),防止后台死循环造成内存泄漏与性能损耗。
基于JavaScript经典实例的动画效果实现代码
图 4:JavaScript 动画工程化实现与性能优化四大支柱

七、总结

基于 JavaScript 实现动画效果的本质,是运用帧调度机制对随时间变化的数值进行数学插值。通过掌握 requestAnimationFrame 的生命周期、配合科学的缓动曲线,并在编写代码时坚持“使用合成层属性”与“做好重入中断管理”,开发者既能实现高度自由的交互特效,又能确保页面在各种设备上始终保持流畅丝滑的运行表现。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章