Skip to content

DOM 对象深入分析(Day16 3/3) ​

本页对应 Day16_DOM,共 5 段视频,约 1 小时 6 分。

学习目标 ​

  • 能够使用 HTML 与 CSS 搭建轮播图结构,完成图片列表、底部按钮、左右箭头的布局,并实现默认显示第一张图片及对应按钮激活样式。
  • 能够使用 JavaScript 为底部按钮绑定点击事件,实现按钮选中状态排他、对应图片切换,并说明双重遍历中事件回调的执行时机。
  • 能够通过统一索引变量同步底部按钮与左右箭头,实现上一张、下一张切换,并处理索引越界后的首尾循环。
  • 能够使用定时器实现轮播图自动播放,封装切换函数,并实现鼠标移入停止、移出继续播放。
  • 能够使用 opacity 配合 CSS transition 替代 display 切换,实现轮播图图片淡入淡出效果,并说明图片绝对定位叠加的必要性。

知识点 ​

轮播图布局:先把结构搭稳 ​

为什么需要:轮播图形式很多,但布局结构基本一致,先把“容器—图片列表—按钮列表—左右箭头”搭好,后面切图逻辑才有稳定落点。怎么想:外层用定位容器,图片和按钮都用列表;图片默认只显示一张,其余隐藏;按钮和箭头覆盖在图片上。怎么做:HTML 保留图片和按钮结构,CSS 给容器宽高与相对定位,给图片 li 绝对定位并叠在一起。易错点:display:none 不占位,后面做淡入淡出时不能直接替代 opacity;箭头图是精灵图,右箭头要改 background-position。

html
<div class="play-books" id="play-books">
  <ul class="image">
    <li class="active"><a href="#"><img src="image/01.jpg" alt="轮播图第一张"></a></li>
    <li><a href="#"><img src="image/02.jpg" alt="轮播图第二张"></a></li>
    <li><a href="#"><img src="image/03.jpg" alt="轮播图第三张"></a></li>
    <li><a href="#"><img src="image/04.jpg" alt="轮播图第四张"></a></li>
    <li><a href="#"><img src="image/05.jpg" alt="轮播图第五张"></a></li>
  </ul>
  <ol class="btns">
    <li class="active">1</li><li>2</li><li>3</li><li>4</li><li>5</li>
  </ol>
  <span class="arrow prev"></span>
  <span class="arrow next"></span>
</div>
css
.play-books { position: relative; width: 900px; height: 350px; margin: 100px auto; }
.image li { position: absolute; left: 0; top: 0; width: 900px; height: 350px; opacity: 0; transition: opacity .6s; }
.image li.active { opacity: 1; }
.btns { position: absolute; right: 10px; bottom: 10px; display: flex; }
.btns li { width: 20px; height: 20px; margin-left: 10px; border: 1px solid #fff; border-radius: 50%; background: #000; color: #fff; text-align: center; line-height: 20px; cursor: pointer; }
.btns li.active { color: #f60; border-color: #f60; }
.arrow { position: absolute; top: 50%; width: 40px; height: 100px; transform: translateY(-50%); background-image: url("image/arrow.png"); cursor: pointer; }
.arrow.prev { left: 0; }
.arrow.next { right: 0; background-position: -50px 0; }

点击底部按钮切换:索引与排他 ​

为什么需要:点哪个按钮就显示哪张图,同时只能有一个按钮处于选中态。怎么想:按钮和图片数量一致、索引一一对应,用一个 index 记录当前项;每次先“排他”清空所有 active,再给当前项加 active。怎么做:给每个按钮绑点击事件,点击时把 index 改成当前按钮索引,再调用统一的 setActive()。易错点:不要只给当前按钮加类而忘了清空旧按钮;转写稿里提到“按钮 forEach 里套图片 forEach”,执行时间不同,但更稳妥是抽成 setActive() 统一处理。

js
function setActive() {
  imageItems.forEach(item => item.classList.remove('active'));
  btnItems.forEach(item => item.classList.remove('active'));
  imageItems[index].classList.add('active');
  btnItems[index].classList.add('active');
}

btnItems.forEach((btn, i) => {
  btn.addEventListener('click', () => {
    index = i;
    setActive();
  });
});

左右箭头与同步:用 index 做单一数据源 ​

为什么需要:底部按钮、左右箭头、自动播放如果各改各的,就会不同步。怎么想:让它们都只改同一个 index,再统一调用 setActive(),就能同步。怎么做:封装 next() 和 prev();右箭头和自动播放调用 next(),左箭头调用 prev()。易错点:越界。转写稿调试时,到第 5 张再点下一个会找不到元素并报错,所以 next() 里判断 index > btnItems.length - 1 就回到 0;prev() 里判断 index < 0 就回到最后一项。另注意变量名写错会报错,转写稿里就出现过 BT index 少字母、intervalId 少 G 导致停不下来的调试过程。

js
function next() {
  index++;
  if (index > btnItems.length - 1) index = 0;
  setActive();
}
function prev() {
  index--;
  if (index < 0) index = btnItems.length - 1;
  setActive();
}
nextArrow.addEventListener('click', next);
prevArrow.addEventListener('click', prev);

自动播放与暂停:定时器与封装 ​

为什么需要:轮播图要自动切换,鼠标移入时又要暂停,方便用户查看。怎么想:定时器每隔一段时间调用 next();鼠标进入清定时器,离开重新设置。怎么做:用 intervalId 保存定时器标记,mouseenter 里 clearInterval(intervalId),mouseleave 里重新 setInterval(next, duration)。易错点:重新设置定时器时不要再写 let intervalId,否则停的是旧标记;转写稿里还提到相同代码不要写两遍,所以把“切下一张”封装成 next(),定时器直接传函数名,不要写成 next()。

js
let intervalId = setInterval(next, 3000);
playBooks.addEventListener('mouseenter', () => clearInterval(intervalId));
playBooks.addEventListener('mouseleave', () => {
  intervalId = setInterval(next, 3000);
});

淡入淡出:用 opacity 替代 display ​

为什么需要:display:none/block 切换很生硬,且 CSS 过渡不能作用在 display 上。怎么想:用 opacity:0/1 控制显隐,opacity 是纯数字,可以加过渡。怎么做:给图片 li 绝对定位并全部叠在 left:0; top:0,非激活 opacity:0,激活 opacity:1,再加 transition: opacity .6s。易错点:只把 display 换成 opacity 会发现五张图仍占位、层叠错乱,因为 opacity:0 的元素还占位置;所以必须配合绝对定位。转写稿中先改 opacity 发现错乱,再给 li 加绝对定位,最后加过渡,这个推导顺序要理解。

本节作业说明 ​

本节作业是“滑动效果轮播图”。转写稿里课堂先把轮播图的布局、底部按钮切换、左右箭头切换、自动播放、鼠标悬停暂停跑通,最后用 opacity 做了淡入淡出。若作业要求“滑动”,应替换图片显示方式;但转写稿未展开位移滑动实现,本文不编造。下面把课堂推导整理成可复用的轮播核心。转写稿里的“轮波图”“Play干 books”“C抬”等,分别纠正为“轮播图”“play-books”“setActive”。

布局:先搭结构再谈效果 ​

为什么需要:轮播图形式很多,但结构基本一致:外层容器包图片列表和按钮列表。
怎么想:外层 .play-books 负责宽高和相对定位;图片用 ul.image > li;底部按钮用 ol.btns > li;左右箭头最后加 span.arrow.prev/.next。
怎么做:

html
<div class="play-books" id="play-books">
  <ul class="image">
    <li class="active"><a href="#"><img src="image/01.jpg" alt="轮播图第一张"></a></li>
    <li><a href="#"><img src="image/02.jpg" alt="轮播图第二张"></a></li>
    <!-- 其余图片同理 -->
  </ul>
  <ol class="btns">
    <li class="active">1</li><li>2</li><li>3</li><li>4</li><li>5</li>
  </ol>
  <span class="arrow prev"></span>
  <span class="arrow next"></span>
</div>

CSS 关键:图片 900×350,默认只显示 .active;按钮绝对定位到图片上;箭头宽 40 高 100,纵向居中用 top:50%; transform:translateY(-50%)。
易错点:ul/ol 默认有 list-style 和内边距,要先清掉;图片设 display:block 去掉底部空白;按钮和箭头要定位,父级必须 position:relative。

底部按钮切换与排他 ​

为什么需要:点哪个按钮,哪张图显示,且原来选中的按钮要取消。
怎么想:这跟选项卡几乎一样:先全部取消,再给当前项加 active。
怎么做:

js
const playBooks = document.querySelector('#play-books');
const imageItems = playBooks.querySelectorAll('.image li');
const btnItems = playBooks.querySelectorAll('.btns li');
let index = 0;

function setActive() {
  imageItems.forEach(item => item.classList.remove('active'));
  btnItems.forEach(item => item.classList.remove('active'));
  imageItems[index].classList.add('active');
  btnItems[index].classList.add('active');
}

btnItems.forEach((btn, btnIndex) => {
  btn.addEventListener('click', () => {
    index = btnIndex;
    setActive();
  });
});

易错点:外层 forEach 只是注册事件,点击回调稍后才执行;回调里的 btnIndex 与外层变量不要混。转写稿中曾把 btnIndex 拼错导致报错,调试时看控制台行号即可。

左右箭头与 index 同步 ​

为什么需要:底部按钮和左右箭头不能各玩各的,必须控制同一套“当前索引”。
怎么想:用上层作用域的 index 记录当前图片和按钮索引;点按钮改 index,点箭头也改 index,最后统一调 setActive()。
怎么做:

js
function next() {
  index++;
  if (index > btnItems.length - 1) index = 0;
  setActive();
}
function prev() {
  index--;
  if (index < 0) index = btnItems.length - 1;
  setActive();
}
nextArrow.addEventListener('click', next);
prevArrow.addEventListener('click', prev);

易错点:到第一张再点“上一个”要回到最后一张;到最后一张再点“下一个”要回到第一张。不要写死 4,用 btnItems.length - 1 更稳。

自动播放与鼠标悬停暂停 ​

为什么需要:轮播图通常自动切换,但用户想细看时应暂停。
怎么想:定时器只做“index 递增 + 调 next”;鼠标进入清定时器,离开重新开。
怎么做:

js
const duration = 3000;
let intervalId = setInterval(next, duration);

playBooks.addEventListener('mouseenter', () => {
  clearInterval(intervalId);
});
playBooks.addEventListener('mouseleave', () => {
  intervalId = setInterval(next, duration);
});

易错点:intervalId 拼写要一致;离开后重新赋值给同一个变量,否则下次停不下来。转写稿里就出现过变量少字母导致暂停失效的调试过程。

淡入淡出:用 opacity 替代 display ​

为什么需要:display:none/block 不能加过渡,切换很生硬。
怎么想:用 opacity:0/1 控制显隐,再给 opacity 加 transition。但 opacity:0 仍占位置,所以图片 li 要绝对定位叠在一起。
怎么做:

css
.image li {
  position: absolute;
  left: 0;
  top: 0;
  width: 900px;
  height: 350px;
  opacity: 0;
  transition: opacity .6s;
}
.image li.active { opacity: 1; }

易错点:只改 opacity 不定位,五张图会按文档流排开;过渡时间不宜过长,课堂用 400–600ms。作业若要求“滑动”,应把显示方式换成位移方案,而不是只改 opacity。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

代码示例 ​

代码 1:轮播图布局 + 点击底部按钮切换图片 ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>轮播图-布局与底部按钮切换</title>
<style>
  /* ---------- 全局重置 ---------- */
  * { margin: 0; padding: 0; }
  ul, ol { list-style: none; }
  img { display: block; }          /* 去掉图片底部空白 */

  /* ---------- 外层容器 ---------- */
  .play-books {
    position: relative;            /* 子元素定位的包含块 */
    width: 900px;
    height: 350px;
    margin: 100px auto;
    overflow: hidden;
  }

  /* ---------- 图片列表 ---------- */
  .play-books .image li {
    width: 900px;
    height: 350px;
    display: none;                 /* 默认全部隐藏 */
  }
  .play-books .image li.active {   /* 权重更高,覆盖上面的 none */
    display: block;
  }

  /* ---------- 底部按钮 ---------- */
  .play-books .btns {
    position: absolute;            /* 叠到图片上面,必须定位 */
    right: 20px;
    bottom: 20px;
    display: flex;                 /* 让 5 个圆点横着排 */
  }
  .play-books .btns li {
    width: 20px;
    height: 20px;
    margin-left: 10px;
    background: #000;
    color: #fff;
    border: 1px solid #fff;
    text-align: center;
    line-height: 20px;             /* 文字纵向居中 */
    border-radius: 50%;            /* 变成圆形 */
    cursor: pointer;               /* 小手 */
  }
  .play-books .btns li.active {
    color: #f60;
    border-color: #f60;
  }

  /* ---------- 左右箭头 ---------- */
  .play-books .arrow {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);   /* 只做纵向居中 */
    width: 40px;
    height: 100px;
    background-image: url(images/arrow.png);  /* 精灵图,自行替换 */
    cursor: pointer;
  }
  .play-books .arrow.prev { left: 0; }
  .play-books .arrow.next {
    right: 0;
    background-position: -50px 0;  /* 只改 X 坐标,取右侧那半张 */
  }
</style>
</head>
<body>

<div class="play-books" id="play-books">
  <!-- 图片列表 -->
  <ul class="image">
    <li class="active"><a href="#"><img src="images/01.jpg" alt=""></a></li>
    <li><a href="#"><img src="images/02.jpg" alt=""></a></li>
    <li><a href="#"><img src="images/03.jpg" alt=""></a></li>
    <li><a href="#"><img src="images/04.jpg" alt=""></a></li>
    <li><a href="#"><img src="images/05.jpg" alt=""></a></li>
  </ul>

  <!-- 底部按钮列表 -->
  <ol class="btns">
    <li class="active">1</li>
    <li>2</li>
    <li>3</li>
    <li>4</li>
    <li>5</li>
  </ol>

  <!-- 左右箭头 -->
  <span class="arrow prev"></span>
  <span class="arrow next"></span>
</div>

<script>
(function () {
  // 1. 获取元素
  var playBooks  = document.querySelector('#play-books');
  var imageItems = playBooks.querySelectorAll('.image li');  // 所有图片(的 li)
  var btnItems   = playBooks.querySelectorAll('.btns li');   // 所有按钮

  // 2. 给每个按钮绑定点击事件
  btnItems.forEach(function (btnItem, index) {
    btnItem.onclick = function () {
      // 排他:先把所有按钮、所有图片都取消选中/隐藏
      btnItems.forEach(function (item, i) {
        item.classList.remove('active');
        imageItems[i].classList.remove('active');   // 索引一一对应,可顺手一起排
      });
      // 再让当前按钮、对应图片激活
      this.classList.add('active');
      imageItems[index].classList.add('active');
    };
  });
})();
</script>
</body>
</html>

要点与易错点

  • .play-books 必须写 position: relative,否则底部按钮和左右箭头的 absolute 会跑到页面上找别的包含块。
  • .play-books .image li 默认 display: none,.active 那条 display: block 权重更高,才能覆盖,这是“一次只显示一张”的关键。
  • 左右箭头用 top: 50% + transform: translateY(-50%) 做纵向居中,只处理 Y 轴,不要写成 translate(-50%, -50%)。
  • 排他操作的本质是“先全部取消,再单独激活”,顺序不能反。
  • forEach 里嵌套 forEach 不冲突:外层只负责绑定事件,内层回调要等点击时才执行,两者作用域和索引互不影响。
  • 图片 li 和按钮 li 数量必须一致,才能用同一个 index 一起排他。

代码 2:完整版(左右箭头 + 自动播放 + 鼠标移入暂停 + 淡入淡出) ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>轮播图-完整版</title>
<style>
  * { margin: 0; padding: 0; }
  ul, ol { list-style: none; }
  img { display: block; }

  .play-books {
    position: relative;
    width: 900px;
    height: 350px;
    margin: 100px auto;
    overflow: hidden;
  }

  /* ---------- 关键改动:用 opacity 代替 display ---------- */
  .play-books .image li {
    position: absolute;            /* 5 张图必须叠在一起,否则会各占一行位置 */
    top: 0;
    left: 0;
    width: 900px;
    height: 350px;
    opacity: 0;                    /* 0 = 看不见,替代 display:none */
    transition: opacity .6s;       /* display 不能过渡,opacity 可以 */
  }
  .play-books .image li.active {
    opacity: 1;                    /* 1 = 完全显示 */
  }

  .play-books .btns {
    position: absolute;
    right: 20px;
    bottom: 20px;
    display: flex;
    z-index: 2;                    /* 保证圆点盖在图片上面 */
  }
  .play-books .btns li {
    width: 20px;
    height: 20px;
    margin-left: 10px;
    background: #000;
    color: #fff;
    border: 1px solid #fff;
    text-align: center;
    line-height: 20px;
    border-radius: 50%;
    cursor: pointer;
  }
  .play-books .btns li.active {
    color: #f60;
    border-color: #f60;
  }

  .play-books .arrow {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    width: 40px;
    height: 100px;
    background-image: url(images/arrow.png);
    cursor: pointer;
    z-index: 2;
  }
  .play-books .arrow.prev { left: 0; }
  .play-books .arrow.next { right: 0; background-position: -50px 0; }
</style>
</head>
<body>

<div class="play-books" id="play-books">
  <ul class="image">
    <li class="active"><a href="#"><img src="images/01.jpg" alt=""></a></li>
    <li><a href="#"><img src="images/02.jpg" alt=""></a></li>
    <li><a href="#"><img src="images/03.jpg" alt=""></a></li>
    <li><a href="#"><img src="images/04.jpg" alt=""></a></li>
    <li><a href="#"><img src="images/05.jpg" alt=""></a></li>
  </ul>
  <ol class="btns">
    <li class="active">1</li>
    <li>2</li>
    <li>3</li>
    <li>4</li>
    <li>5</li>
  </ol>
  <span class="arrow prev"></span>
  <span class="arrow next"></span>
</div>

<script>
(function () {
  // ---------- 1. 获取元素 ----------
  var playBooks  = document.querySelector('#play-books');
  var imageItems = playBooks.querySelectorAll('.image li');
  var btnItems   = playBooks.querySelectorAll('.btns li');
  var prevArrow  = playBooks.querySelector('.arrow.prev');
  var nextArrow  = playBooks.querySelector('.arrow.next');

  // ---------- 2. 关键状态变量 ----------
  var index    = 0;        // 记录当前显示图片 / 选中按钮的索引(同步的核心)
  var duration = 3000;     // 轮播间隔,一般 3~5 秒
  var timerId  = null;     // 定时器标记,暂停时要靠它清掉

  // ---------- 3. 按 index 统一刷新视图 ----------
  function setActive() {
    imageItems.forEach(function (item) {
      item.classList.remove('active');
    });
    btnItems.forEach(function (item) {
      item.classList.remove('active');
    });
    imageItems[index].classList.add('active');
    btnItems[index].classList.add('active');
  }

  // ---------- 4. 下一张 / 上一张(含边界判断) ----------
  function next() {
    index++;
    if (index > btnItems.length - 1) {   // 超过最后一张就回到 0
      index = 0;
    }
    setActive();
  }

  function prev() {
    index--;
    if (index < 0) {                     // 小于 0 就跳到最后一个
      index = btnItems.length - 1;
    }
    setActive();
  }

  // ---------- 5. 底部按钮:只改 index,再调用 setActive ----------
  btnItems.forEach(function (btnItem, i) {
    btnItem.onclick = function () {
      index = i;          // 改的是外层那个 index,不是回调参数
      setActive();
    };
  });

  // ---------- 6. 左右箭头 ----------
  nextArrow.onclick = next;   // 注意:传函数引用,不加括号
  prevArrow.onclick = prev;

  // ---------- 7. 自动播放 ----------
  timerId = setInterval(next, duration);

  // ---------- 8. 鼠标移入暂停 / 移出继续 ----------
  playBooks.onmouseenter = function () {
    clearInterval(timerId);
  };
  playBooks.onmouseleave = function () {
    timerId = setInterval(next, duration);   // 重新赋值给同一个变量,下次才能清掉
  };
})();
</script>
</body>
</html>

要点与易错点

  • index 是同步的核心:底部按钮、左右箭头、自动播放三者都不直接操作 DOM,只改 index,再统一调 setActive()。这样三个入口控制的是同一套系统,不会各玩各的。
  • 边界判断不能省:next() 里 index > btnItems.length - 1 就归 0,prev() 里 index < 0 就取 length - 1。不判断的话索引会越界,imageItems[index] 是 undefined,classList 直接报错。
  • setActive() 里的 index 不要加形参:如果写成 function setActive(index),那就是在函数内部重新定义了一个变量,外面的 index 就失效了,整个同步逻辑会崩。
  • setInterval(next, duration) 不能写成 setInterval(next(), duration),加了括号是立刻执行并把返回值当回调。
  • onmouseleave 里必须写 timerId = setInterval(...):如果不赋值(或写成 var timerId = ...),新定时器的标记就存不进外层那个变量,下次 clearInterval(timerId) 清掉的还是旧标记,鼠标移入就停不下来了。
  • 淡入淡出必须改两处 CSS:一是 li 加 position: absolute; top: 0; left: 0(否则 opacity: 0 的元素仍占位置,五张图会上下排开);二是把 display 换成 opacity,因为 display 无法参与 transition。
  • transition 加在 .image li 上,时间不宜太长,转写稿里最终用的是 600ms 左右,再长就显得拖沓。
  • background-position: -50px 0 里只改 X 坐标(负数往左推一张图的宽度),Y 保持 0;写成两个正数或写错轴,箭头就显示成和左边一样。

常见错误 ​

  1. 切换图片时只有“啪”一下的硬切,加不上过渡 现象:给 display 的 none/block 加了 transition,但图片仍然瞬间跳变。 原因:display 不是可过渡属性,值从 none 变 block 属于离散跳变,过渡规则对它无效。 修法:改用 opacity: 0 / 1 控制显隐,并把 transition 写在真正变化的那个属性上(transition: opacity .6s),时间控制在 400–600ms。

  2. 改成 opacity 后五张图全叠在一起、位置错乱 现象:第一张正常,后面的图往下排,或最后一张盖住前面所有。 原因:opacity: 0 的元素仍然占据文档流位置,只是看不见,与 display: none 的“不占位”完全不同。 修法:给每个 li 加 position: absolute; left: 0; top: 0,让五张图在父容器的同一个坐标点叠放,父容器保持 position: relative。

  3. 点到最后一张再点“下一个”,图片全没了,控制台报错 现象:Uncaught TypeError: Cannot read properties of undefined (reading 'classList')。 原因:index 没有做边界处理,越界后 imgItems[index] 是 undefined,后续 classList.add 直接抛错。 修法:改索引的函数里先判断——index < 0 时置为 list.length - 1,index > list.length - 1 时置为 0;长度用集合的 length,不要写死 4。

  4. 鼠标移入后定时器停不下来,或移出后越切越快 现象:鼠标放上去还在自动播放;或者移出几次后图片切换频率翻倍。 原因:setInterval 的返回值没有存到共享变量里,clearInterval 收到的是 undefined;或者每次移出都新建一个定时器而没有先清掉旧的,导致多个定时器叠加。 修法:在事件回调外层用同一个上层作用域变量保存 id,移入时 clearInterval(timerId),移出时先清再 timerId = setInterval(next, delay),全局只保留一个定时器。

  5. 点新按钮后,旧按钮的高亮还在(排他没做干净) 现象:点了第 3 个圆点,第 1 个圆点依然是选中色,两个同时亮。 原因:只在回调里给自己 add('active'),没有先把所有按钮、所有图片的 active 清掉;或者清的时候用了 querySelector 只拿到第一个元素。 修法:每次点击回调开头先遍历全部按钮 remove('active')、遍历全部图片 remove('active'),再给当前 index 对应的按钮和图片 add('active')。排他逻辑和“显示当前项”必须成对出现。

  6. 函数里重新声明了 index,导致点击按钮图片不动 现象:圆点高亮正常,但图片始终停在第一张;或箭头点击完全没反应。 原因:在函数内部写了 let index = ... 或 var index = ...,产生了新的局部变量,遮蔽了外层用于同步的共享 index,外层值从未被修改。 修法:函数内只做赋值:index = btnIndex,不加 let/var/const;改完立刻调用统一的渲染函数。

  7. 把 setInterval 的回调写成 setInterval(next(), delay) 现象:页面一打开图片立刻跳一张然后不动了,或者点击箭头后自动播放消失。 原因:next() 是立即调用,传给定时器的是它的返回值(undefined),定时器等于什么都没执行。 修法:传函数引用 setInterval(next, delay),需要传参时用箭头函数包一层。

练习 ​

  1. 让轮播图不依赖固定张数 把 5 个 li 改成由 JS 数组渲染生成(数组里放图片地址),然后往数组里加到 7 张。要求:不改任何边界判断代码,点击箭头仍能正确循环。验收点:从第 7 张点“下一个”回到第 1 张,不报错。

  2. 手动操作后重置自动播放 现在点完箭头,定时器仍按原节奏触发,会出现“刚点完立刻又跳一张”。要求:任何一次手动切换(点圆点或箭头)后,自动播放的计时从 0 重新开始。写完后说明你把重置逻辑放在哪一层、为什么放这里最省代码。

  3. 给轮播图加页码指示 在右下角显示 3 / 5 这样的当前页文本,随图片同步更新。验收点:自动播放、点圆点、点箭头三种方式下页码都正确;鼠标悬停暂停时页码不变。

  4. 改用滑动效果(对应作业) 把淡入淡出换成横向滑动:所有图并排放在一个宽为 5 × 900px 的轨道里,通过 transform: translateX(-index * 900px) 移动,并加过渡。要求:改完后原有三个功能(圆点、箭头、自动播放)全部保持正常。思考:li 还需要 position: absolute 吗?边界判断代码需要改吗?

  5. 键盘控制 + 空格暂停 支持左右方向键切换、空格键切换播放/暂停状态。要求暂停后鼠标移入移出不再恢复播放,需要空格再恢复。验收点:三种控制方式(键盘、鼠标点击、自动)互不冲突,不会出现两个定时器同时在跑。

原视频对照 ​

#视频时长
1轮播图-布局17:05
2轮播图-点击底部按钮切换图片10:54
3轮播图-点击左右箭头切换图片17:15
4轮播图-自动播放15:54
5轮播图-淡入淡出效果05:50

小结 ​

  1. 轮播图的布局结构是通用的:一个定位父容器 + 叠放的图片列表 + 定位在底部的按钮列表 + 定位在两侧的箭头,形式再花哨也逃不出这个骨架。
  2. 让多张图“只显示一张”有两种实现,display 和 opacity 的区别不是审美问题,而是能不能加过渡的问题;要效果就必须选可过渡属性,代价是必须自己处理叠放定位。
  3. 整个轮播图真正的核心只有一个变量——当前索引。圆点、箭头、自动播放三者能同步,靠的不是互相调用,而是都只修改同一个索引,再由同一个渲染函数统一表现。
  4. 因此正确的封装顺序是:先有渲染函数,再让所有入口去改索引,最后让自动播放也走同一个“切下一张”函数。任何一处绕过索引直接操作 DOM,都会埋下不同步的隐患。
  5. 索引的边界判断必须基于集合的 length 而不是硬编码数字,这是后续能否低成本改张数的分水岭。
  6. 定时器的两个必备动作:返回值必须被保存下来、再次创建前必须确保旧定时器已清除。这两点做不到,就会出现“停不下来”和“越来越快”两个经典症状。
  7. 排他操作和显示当前项要写在一起、成对出现;只加不清或只清不加,都会出现两个高亮或全都不亮的中间态。
  8. 当发现同一段逻辑在第二处出现时,就应该立刻抽成函数——自动播放和点击箭头共用“切下一张”,是本次重构中最值得做的一次抽象。