JS-从基础到进阶,原生 JS 手写无缝轮播图

60 阅读4分钟

前言

轮播图(Carousel)是前端开发中最基础也最经典的组件之一。看似简单,但要实现丝滑、无缝的循环切换效果,其实隐藏着许多关于 DOM 操作和 CSS 动画的细节。本文将带你从基础版过渡到进阶无缝版。

一、 基础版:基于 Transform 的位移实现

1. 核心原理

  1. 外层容器:设置 overflow: hidden,宽度固定。
  2. 内层轨道:使用 display: flex 让图片横向排列。
  3. 位移逻辑:通过修改轨道的 transform: translateX() 值来实现切换。

2. 存在的问题

当你从最后一张点击“下一张”时,为了回到第一张,轨道会向相反方向快速滑过中间所有图片。这种“视觉回退”体验很差,无法达到真正的“循环”感。

<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>简单轮播图 - 方法1</title>
    <style>
      * {
        margin: 0;
        padding: 0;
        box-sizing: border-box;
      }

      body {
        display: flex;
        justify-content: center;
        align-items: center;
        height: 100vh;
        background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
      }

      .container {
        width: 800px;
        background: white;
        border-radius: 10px;
        padding: 20px;
      }

      h1 {
        text-align: center;
        color: #333;
        margin-bottom: 20px;
      }

      /* 轮播图容器 */
      .carousel-container {
        width: 100%;
        height: 400px;
        position: relative;
        overflow: hidden;
        border-radius: 8px;
      }

      /* 轮播图列表 */
      .carousel-list {
        display: flex;
        height: 100%;
        transition: transform 0.5s ease-in-out;
      }

      /* 轮播图项 */
      .carousel-slide {
        min-width: 100%;
        height: 100%;
        display: flex;
        justify-content: center;
        align-items: center;
        font-size: 3rem;
        color: white;
      }

      /* 左右导航按钮 */
      .carousel-button {
        position: absolute;
        top: 50%;
        transform: translateY(-50%);
        background: rgba(0, 0, 0, 0.5);
        color: white;
        border: none;
        width: 40px;
        height: 40px;
        border-radius: 50%;
        cursor: pointer;
        font-size: 1.2rem;
        transition: background 0.3s;
      }

      .carousel-button:hover {
        background: rgba(0, 0, 0, 0.8);
      }

      .button-prev {
        left: 10px;
      }

      .button-next {
        right: 10px;
      }

    </style>
  </head>
  <body>
    <div class="container">
      <h1>简单轮播图 - 方法1: transform实现</h1>

      <div class="carousel-container" id="carousel-container">
        <div class="carousel-list" id="carousel-list">
          <!-- 图片项,用不同颜色表示不同图片 -->
          <div class="carousel-slide" style="background: #ff6b6b">图片 1</div>
          <div class="carousel-slide" style="background: #4ecdc4">图片 2</div>
          <div class="carousel-slide" style="background: #45b7d1">图片 3</div>
          <div class="carousel-slide" style="background: #96ceb4">图片 4</div>
          <div class="carousel-slide" style="background: #ffeaa7">图片 5</div>
        </div>

        <!-- 导航按钮 -->
        <button id="button-prev" class="carousel-button button-prev"></button>
        <button id="button-next" class="carousel-button button-next"></button>
      </div>
    </div>

    <script>
      const carouselContainer = document.getElementById('carousel-container') //轮播图容器
      const carouselList = document.getElementById("carousel-list"); // 轮播列表
      const slides = Array.from(carouselList.children); // 被轮播的图片
      const prevButton = document.getElementById("button-prev"); //上一张按钮
      const nextButton = document.getElementById("button-next"); //下一张按钮

      let currentIndex = 0; // 当前轮播的图片索引
      const slideWidth = slides[0].getBoundingClientRect().width; // 显示的图片宽度


      // 移动到指定索引
      function moveToSlide(index) {
        // 处理边界
        if (index < 0) index = slides.length - 1;
        if (index >= slides.length) index = 0;

        // 移动轨道
        carouselList.style.transform = "translateX(-" + slideWidth * index + "px)";

        // 更新当前索引
        currentIndex = index;

      }


      // 下一张
      function nextSlide() {
        moveToSlide(currentIndex + 1);
      }

      // 上一张
      function prevSlide() {
        moveToSlide(currentIndex - 1);
      }

      // 事件监听
      prevButton.addEventListener("click", prevSlide);
      nextButton.addEventListener("click", nextSlide);
    </script>
  </body>
</html>


二、 进阶版:无缝循环轮播(Seamless Loop)

1. 核心改进思路:克隆节点

为了实现无限向后滚动的假象,我们需要在轨道的头部和尾部做“手脚”:

  • 克隆第一张图片放到列表最后。
  • 克隆最后一张图片放到列表最前。

2. 实现流程

  1. 初始偏移:由于开头插入了克隆节点,初始 currentIndex 设为 1

  2. 正常滑动:点击时正常开启动画滑动。

  3. 瞬间瞬移(核心)

    • 监听轨道的 transitionend 事件。
    • 当动画结束,发现当前在“最后一张克隆图”时,立即关闭动画(transition: none) ,并瞬间跳回“真正的第一张”。
<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>无缝轮播图 - 改进版</title>
    <style>
      * { margin: 0; padding: 0; box-sizing: border-box; }
      body {
        display: flex; justify-content: center; align-items: center;
        height: 100vh; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
      }
      .container { width: 800px; background: white; border-radius: 10px; padding: 20px; }
      h1 { text-align: center; color: #333; margin-bottom: 20px; }

      .carousel-container {
        width: 100%; height: 400px; position: relative;
        overflow: hidden; border-radius: 8px;
      }

      .carousel-list {
        display: flex; height: 100%;
        /* 初始不加 transition,由 JS 控制开关 */
      }

      .carousel-slide {
        min-width: 100%; height: 100%;
        display: flex; justify-content: center; align-items: center;
        font-size: 3rem; color: white;
      }

      .carousel-button {
        position: absolute; top: 50%; transform: translateY(-50%);
        background: rgba(0, 0, 0, 0.5); color: white; border: none;
        width: 40px; height: 40px; border-radius: 50%; cursor: pointer;
        font-size: 1.2rem; transition: background 0.3s; z-index: 10;
      }
      .button-prev { left: 10px; }
      .button-next { right: 10px; }
    </style>
  </head>
  <body>
    <div class="container">
      <h1>无缝轮播图 - 改进版</h1>
      <div class="carousel-container">
        <div class="carousel-list" id="carousel-list">
          <div class="carousel-slide" style="background: #ff6b6b">图片 1</div>
          <div class="carousel-slide" style="background: #4ecdc4">图片 2</div>
          <div class="carousel-slide" style="background: #45b7d1">图片 3</div>
          <div class="carousel-slide" style="background: #96ceb4">图片 4</div>
          <div class="carousel-slide" style="background: #ffeaa7">图片 5</div>
        </div>

        <button id="button-prev" class="carousel-button button-prev"></button>
        <button id="button-next" class="carousel-button button-next"></button>
      </div>
    </div>

    <script>
      const carouselList = document.getElementById("carousel-list");
      console.log(carouselList)
      const slides = Array.from(carouselList.children);
      const prevButton = document.getElementById("button-prev");
      const nextButton = document.getElementById("button-next");

      // 1. 克隆第一张和最后一张
      const firstClone = slides[0].cloneNode(true);
      const lastClone = slides[slides.length - 1].cloneNode(true);

      // 2. 将克隆节点添加到列表
      carouselList.appendChild(firstClone);
      carouselList.insertBefore(lastClone, slides[0]);

      // 3. 重新计算状态
      let currentIndex = 1; // 因为前面插入了一个,所以初始索引为 1
      const slideWidth = slides[0].getBoundingClientRect().width; // 使用百分比计算更稳健
      console.log(slides[0].getBoundingClientRect().width)
      // 初始化显示第一张(不是克隆的第0张)
      console.log(`translateX(-${currentIndex * slideWidth})`)
      carouselList.style.transform = `translateX(-${currentIndex * slideWidth}px)`;

      function moveToSlide(index, hasAnimation = true) {
        currentIndex = index;
        // 开启动画
        carouselList.style.transition = hasAnimation ? "transform 0.5s ease-in-out" : "none";
        carouselList.style.transform = `translateX(-${currentIndex * slideWidth}px)`;
      }

      // 核心:监听过渡结束
      carouselList.addEventListener('transitionend', () => {
        
        // 如果到了最后的克隆图,瞬间跳回真正的第一张
        if (currentIndex === carouselList.children.length - 1) {
          carouselList.style.transition = "none";
          currentIndex = 1;
          carouselList.style.transform = `translateX(-${currentIndex * slideWidth}px)`;
        }
        
        // 如果到了最前的克隆图,瞬间跳回真正的最后一张
        if (currentIndex === 0) {
          carouselList.style.transition = "none";
          currentIndex = carouselList.children.length - 2;
          carouselList.style.transform = `translateX(-${currentIndex * slideWidth}px)`;
        }
      });

      nextButton.addEventListener("click", () => moveToSlide(currentIndex + 1));
      prevButton.addEventListener("click", () => moveToSlide(currentIndex - 1));
    </script>
  </body>
</html>