简介:面向前端初学者和希望巩固 DOM 操作的开发者,这份压缩包围绕原生 JavaScript 与 jQuery 协作实现无缝轮播图,重点处理轮播动画、图片切换和首尾衔接等常见练习难点,适合课程作业、组件练习和交互逻辑入门。包内共 3 个文件,由 1 个 HTML 主页面与 2 个 PNG 按钮素材组成,压缩包约 6KB,体量轻巧,便于直接打开阅读、对照修改和嵌入自己的练习页面。已有 301 人学习,可作为小型前端交互案例参考。读者能够从中梳理用原生方法封装元素操作的思路,观察 jQuery 动画在轮播切换中的调用方式,并理解在末尾额外追加一张图片、从而在最后一张切换时形成障眼效果的无缝处理技巧,适合用来拆解页面结构、复盘动画触发时机、排查切换错位问题,并积累可复用的实现经验与调试方向。
1. JavaScript + jQuery 轮播图,为什么值得单独拆一遍
JavaScript + jQuery 实现的轮播图,几乎是每个前端人写的第一个「真组件」:图片一张张轮着走,小圆点跟着亮,鼠标移上去停住,移开继续跑。这份资源就是一个完整的轮播图 demo 压缩包,重点是无缝滚动——滑到最后一张不会「啪」地弹回第一张,而是继续往前滑,视觉上首尾相接。它用原生 JavaScript 管状态、jQuery 管动画和事件,代码量不大,但状态机、定时器、动画回调、事件解绑这几件事一件不落。适合两类人:刚学完 DOM 和 jQuery 选择器,想找个能跑通的完整例子对着抄;或者手里还有 jQuery 老项目要维护,想看清一个轮播图内部到底该怎么组织。下面按「结构 → 链路 → 代码 → 踩坑 → 改造」拆开讲。
2. 目录结构与运行:把一个轮播图 demo 跑起来的第一小时
拿到压缩包,先别急着看 JS。轮播图这类组件的 bug 有相当一部分不在逻辑里,而在结构和样式上:轨道宽度没算对、overflow忘了写、li没有浮动,都会让动画看起来像「滑到一半卡住」。所以第一步是把目录和 DOM 骨架摸清楚,再动手跑。
2.1 压缩包里的文件与各自职责
解压之后典型的结构是这样,文件不多,但每一层的职责划得很清楚:
carousel/ ├── index.html # 页面骨架 + 初始化脚本 ├── css/ │ ├── reset.css # 清默认 margin/padding,避免 li 自带缩进 │ └── carousel.css # 轮播图自身样式,容器/轨道/箭头/圆点 ├── js/ │ ├── jquery.min.js # jQuery 本体,注意版本,1.x 和 3.x 的动画行为有差异 │ └── carousel.js # 轮播逻辑,全文核心 └── images/ ├── 1.jpg ~ 4.jpg # 四张演示图,尺寸建议统一 └── dot.png # 圆点(如果用图片的话)carousel.js是唯一需要逐行读的文件,其余都是配套。reset.css看着可有可无,实际上很关键:ul默认有padding-left: 40px,li默认有list-style,不清掉的话轨道整体会往右偏,第一张图左边露出一条白边,新手经常以为是 JS 算错了偏移量。图片建议统一成同一尺寸(比如 800×450),后面所有宽度计算都基于这个假设,混着不同尺寸的图会让「无缝」变成「跳一下」。
2.2 HTML 骨架:轨道、箭头、圆点三层结构
整个组件就三层,层级越浅越好维护:
<div class="carousel" id="banner"> <!-- 轨道:真正被移动的元素,里面装着所有 li --> <ul class="carousel-list"> <li><a href="#"><img src="images/1.jpg" alt=""></a></li> <li><a href="#"><img src="images/2.jpg" alt=""></a></li> <li><a href="#"><img src="images/3.jpg" alt=""></a></li> <li><a href="#"><img src="images/4.jpg" alt=""></a></li> </ul> <!-- 左右箭头,用 a 标签方便加 hover 样式 --> <a href="javascript:;" class="carousel-arrow prev"><</a> <a href="javascript:;" class="carousel-arrow next">></a> <!-- 小圆点,数量与 li 一致,active 表示当前张 --> <div class="carousel-dots"> <span class="carousel-dot active"></span> <span class="carousel-dot"></span> <span class="carousel-dot"></span> <span class="carousel-dot"></span> </div> </div>结构上有两个约定值得强调。第一,ul是「轨道」,所有位移都作用在它身上,li本身不动,这样动画只需要改一个left值,不用逐张操作。第二,圆点用span而不是li,是因为它不参与轨道布局,放在容器里绝对定位就行;圆点的数量不建议手写死,后面初始化时按li的数量动态生成更稳妥,改图不用改 HTML。
<a href="javascript:;">这种写法是为了避免点箭头时页面跳到顶部,如果你习惯用button,记得把默认样式清掉,否则箭头会变成一个灰底方块。
2.3 CSS 定位体系:为什么轨道必须 absolute
样式这块只有一条主线:容器裁剪、轨道绝对定位、li横向排列。
/* 容器:视口,负责裁掉轨道溢出的部分 */ .carousel { position: relative; /* 给箭头的绝对定位提供参照 */ width: 800px; /* 单张图的宽度,JS 里也要读这个值 */ height: 450px; overflow: hidden; /* 没有它,所有图会并排铺开 */ margin: 40px auto; } /* 轨道:宽度由 JS 计算后写入,这里只定位置 */ .carousel-list { position: absolute; top: 0; left: 0; height: 450px; margin: 0; padding: 0; list-style: none; } .carousel-list li { float: left; width: 800px; /* 必须和容器宽度一致 */ height: 450px; } .carousel-list img { display: block; /* 去掉 img 底部的行内间隙 */ width: 100%; height: 100%; } /* 箭头:默认透明,hover 容器时淡入 */ .carousel-arrow { position: absolute; top: 50%; margin-top: -25px; width: 50px; height: 50px; line-height: 50px; text-align: center; background: rgba(0, 0, 0, .35); color: #fff; text-decoration: none; opacity: 0; transition: opacity .3s; } .carousel:hover .carousel-arrow { opacity: 1; } .carousel-arrow.prev { left: 0; } .carousel-arrow.next { right: 0; } /* 圆点 */ .carousel-dots { position: absolute; left: 0; bottom: 16px; width: 100%; text-align: center; } .carousel-dot { display: inline-block; width: 10px; height: 10px; margin: 0 5px; border-radius: 50%; background: rgba(255, 255, 255, .6); cursor: pointer; } .carousel-dot.active { background: #fff; }轨道用absolute而不是margin-left偏移,原因很实际:absolute的left可以直接被 jQuery 的.animate()做数值动画,而margin-left在某些盒模型下会和li的float打架,出现半像素抖动。li用float: left排成一行,轨道宽度必须等于「张数 × 单张宽度」,这个值 JS 会算出来写进行内样式,所以 CSS 里干脆不写width。
注意:容器宽度和
li宽度必须是同一个值。我见过有人在 CSS 里给容器写width: 100%、给li写死800px,结果宽屏下轨道宽度算出来是 0,动画完全不动。
2.4 起服务与首次运行检查
纯静态页面直接双击index.html也能跑,但我一般会起个本地服务,后面接图片懒加载、改成动态取数据的时候不至于踩file://协议的限制:
# 在项目根目录执行,任选一种 python3 -m http.server 8080 # 或者 npx serve .浏览器打开http://localhost:8080,第一次运行按这个顺序检查,能省掉后面一大半排查时间:
- 四张图是不是横向排成了一行(说明
float和轨道宽度生效了)。 - 容器是不是只露出第一张(说明
overflow: hidden生效了)。 - 控制台有没有 404(
jquery.min.js路径写错是最常见的)。 - 打开 DevTools 选中
.carousel-list,看行内style里的width和left,width应该是「张数 × 800」,left应该是-800px(如果做了首尾克隆)。
第 4 步最关键。很多人调轮播图调半天,其实问题就在这两个数值上,看一眼行内样式就知道是宽度算错了还是偏移量算错了。
3. 核心链路拆解:无缝轮播靠哪三个状态变量撑起来
把结构跑通之后,剩下的问题就是「为什么它能无缝地转」。这部分逻辑其实很薄,薄到只有三个变量,但每一个都对应一类 bug,理解它们比背代码有用得多。
3.1 index / animating / timer:轮播图的全部状态
不管轮播图写得多花哨,内部状态就这三样:
| 变量 | 含义 | 取值范围 | 出错的后果 |
|---|---|---|---|
index | 当前显示第几张(逻辑下标) | 0 ~ length-1 | 圆点高亮错位、点第 3 张跳到第 4 张 |
animating | 动画锁,正在切换时为true | true / false | 连点箭头图片错位、动画叠在一起 |
timer | 自动播放的定时器 ID | setInterval返回值 | 越点越快、hover 后不恢复 |
index用逻辑下标而不是物理下标,是这套代码里最值得学的一个设计。因为做了首尾克隆之后,DOM 里实际有length + 2个li:位置 0 是克隆的最后一张,位置 1 到length是真实图片,位置length+1是克隆的第一张。如果直接用物理下标当状态,那么「当前是第几张」和「圆点该亮哪个」永远差 1,到处都要-1,代码会很难读。用逻辑下标,圆点直接eq(index),物理位置在需要动画时临时算一次,边界就清晰了。
animating这个锁,新手最容易省掉。省掉的后果是:快速点三下「下一张」,三个animate同时排队,第一个回调里index加了 1,第二个回调又加 1,最后画面停在第二张,index却已经是 3,圆点和画面对不上。加锁之后,动画期间的所有点击直接return,宁可丢一次点击,也不让状态错乱。
3.2 无缝滚动的两种方案与选型理由
无缝滚动本质上是「从最后一张到第一张的过渡怎么处理」,常见两种做法:
| 方案 | 做法 | 优点 | 缺点 |
|---|---|---|---|
| 首尾克隆 | 在末尾补第一张、开头补最后一张,滑到克隆张后无动画复位 | 视觉上真正无缝,方向一致 | DOM 多两个节点,需要处理克隆元素 |
| 快速回退 | 从最后一张直接动画回到第一张,方向与前进相反 | 实现简单,不改 DOM | 会有明显的「倒着滑回去」,体验差 |
| translate 复位 | 用transform: translate3d位移,配合transitionend复位 | 性能好,走合成层 | 需要处理过渡中断,jQuery 的.animate()不支持 |
这份 demo 用的是首尾克隆,也是 jQuery 时代最主流的方案,因为它和.animate({left: ...})配合得最自然。核心就两行:
// 末尾补第一张,开头补最后一张,都打上 clone 标记方便销毁时清理 if (this.opts.seamless) { this.$list.append(this.$items.first().clone().addClass('clone')); this.$list.prepend(this.$items.last().clone().addClass('clone')); } // 初始化后把轨道停在「真实第一张」,也就是物理位置 1 this._moveTo(this.index + 1, false);克隆完之后,轨道的初始位置必须是-1 × 单张宽度,也就是物理位置 1,而不是 0。停在 0 的话,用户一进来看到的是克隆的最后一张,圆点却亮着第一个,第一眼就是错的。这个偏移在代码里统一写成index + 1,所有位移计算都基于这个常数偏移,就不会乱。
复位那一步是「无动画」的:滑到克隆的第一张(物理位置length+1)之后,动画一结束立刻css({left: -1 * width})跳回物理位置 1。因为这两张图长得一模一样,用户完全看不出跳变,这就是「无缝」的全部秘密。
3.3 一次 next() 的完整时序:从点击到复位
把一次「下一张」拆成时间线,能看清锁和回调是怎么配合的。假设共 4 张图,当前在最后一张(index = 3):
- 点击箭头,
next()被调用,animating是false,进去。 - 立刻把
animating = true,锁住后续点击。 - 判断
index === length - 1,为真,目标物理位置 =length + 1 = 5(克隆的第一张)。 - 调用
_moveTo(5, true),轨道从当前位置动画滑到-5 × 800 = -4000px。 - 动画结束回调里,把
index改回0,再_moveTo(1, false)瞬间定位到-800px。 animating = false,解锁;_syncDots()把第一个圆点点亮。
如果当前不在最后一张(比如index = 1),流程就短一些:目标物理位置 =index + 2 = 3,动画滑过去,回调里index += 1变成 2,解锁。注意+2不是笔误——物理位置比逻辑下标多 1,而「下一张」的逻辑下标是index + 1,所以物理位置是index + 1 + 1。
这个时序里最容易写错的是改 index 的时机。必须在动画结束的回调里改,不能在调用animate之前改。提前改的话,如果动画被打断(比如用户点了别的),index已经变了但画面没动,状态就脏了。
3.4 自动播放、hover 暂停与后台标签页
自动播放就是一个setInterval,但有两个细节决定了它稳不稳:
Carousel.prototype.play = function () { if (!this.opts.autoplay || this.length <= 1) return; this.pause(); // 关键:先清旧定时器,防止叠加 var self = this; this.timer = setInterval(function () { self.next(); // 复用 next(),自动和手动走同一条链路 }, this.opts.interval); }; Carousel.prototype.pause = function () { if (this.timer) { clearInterval(this.timer); this.timer = null; } };第一,play()里必须先pause()。hover 暂停、移出恢复会反复调用play(),不先清掉旧定时器,就会挂上两个、三个setInterval,表现是「鼠标移出之后轮播越来越快」,这是血泪经验里最常见的一条。第二,定时器里调next()而不是直接操作 DOM,这样自动播放和手动点击共用同一套锁和边界处理,不会出现「自动播放能无缝、手动点击就翻车」的分裂。
还有一个容易被忽略的场景:浏览器标签页切到后台,setInterval会被节流甚至暂停,切回来的时候可能积压了好几次触发。挂个visibilitychange就解决了:
$(document).on('visibilitychange.carousel', function () { document.hidden ? self.pause() : self.play(); });play()里的pause()在这里又救了一次场——哪怕事件触发了多次,定时器也只有一个。
4. 关键代码逐段实读:构造、移动、事件绑定
前面讲的是「为什么这么设计」,这一章把代码摊开看「具体怎么写」。整个carousel.js用 IIFE 包起来,对外只暴露一个$.fn.carousel,内部是构造函数加原型方法,这是 jQuery 插件最经典的组织方式。
4.1 构造函数与配置合并
;(function ($) { 'use strict'; // 默认配置:所有可调参数集中一处,改效果只动这里 var DEFAULTS = { speed: 400, // 单次切换动画时长(ms) interval: 3000, // 自动播放间隔(ms) autoplay: true, // 是否自动播放 seamless: true, // 是否无缝(首尾克隆) pauseOnHover: true, // 鼠标移入暂停 startIndex: 0 // 初始第几张,逻辑下标从 0 开始 }; function Carousel($root, options) { this.$root = $root; this.opts = $.extend({}, DEFAULTS, options); // 浅合并,不污染默认值 this.$list = $root.find('.carousel-list'); this.$dots = $root.find('.carousel-dot'); this.$prev = $root.find('.carousel-arrow.prev'); this.$next = $root.find('.carousel-arrow.next'); this.$items = this.$list.children('li'); this.length = this.$items.length; // 真实张数,不含克隆 this.index = this.opts.startIndex; // 逻辑下标 this.animating = false; // 动画锁 this.timer = null; // 自动播放定时器 this.itemWidth = $root.width(); // 单张宽度 = 容器宽度 this.init(); }$.extend({}, DEFAULTS, options)的第一个参数一定要传空对象。写成$.extend(DEFAULTS, options)的话,用户传的配置会直接改掉DEFAULTS,页面上第二个轮播实例就会继承第一个实例的参数,这种 bug 排查起来非常费劲,因为单看任何一个实例的代码都没问题。
this.itemWidth取的是$root.width(),不是写死的 800。这样只要 CSS 里容器宽度变了,JS 这边自动跟上,不必两处改。注意.width()拿的是内容宽度,不含padding和border,所以容器上别加内边距,否则算出来的宽度会偏小,轨道就会错位。
初始化方法负责四件事:克隆、算宽度、定位、绑事件。
Carousel.prototype.init = function () { if (this.length <= 1) return; // 只有一张图,什么都不用做 // 1. 首尾克隆 if (this.opts.seamless) { this.$list.append(this.$items.first().clone().addClass('clone')); this.$list.prepend(this.$items.last().clone().addClass('clone')); } // 2. 撑开轨道宽度,并把轨道停在真实第一张 var total = this.length + (this.opts.seamless ? 2 : 0); this.$list.css({ width: total * this.itemWidth, height: this.$root.height() }); this._moveTo(this.index + (this.opts.seamless ? 1 : 0), false); // 3. 同步圆点、绑事件、开播 this._syncDots(); this._bind(); this.play(); };这里有个seamless的兼容分支:关掉无缝时不克隆,偏移量也就不加 1。所以代码里到处是this.opts.seamless ? 1 : 0,看着啰嗦,但这是让「无缝」变成可配置项要付的代价。如果你确定项目里永远用无缝,可以直接把这个开关砍掉,代码能少三分之一。
4.2 _moveTo:把动画收口成一个函数
整个组件只有这一个地方真正碰 DOM 的left,所有切换动作最后都走到这里:
/** * 把轨道移动到第 pos 张(物理下标,含克隆) * @param {number} pos 物理下标:0 是克隆的末张,1 是真实首张 * @param {boolean} animate 是否用动画过渡 * @param {Function} [done] 动画结束后的回调 */ Carousel.prototype._moveTo = function (pos, animate, done) { var css = { left: -pos * this.itemWidth }; if (!animate) { this.$list.css(css); // 无动画直接定位,肉眼不可见 done && done(); return; } this.$list.stop(true, true).animate(css, this.opts.speed, function () { done && done(); }); };参数含义直接决定调用方式:pos是物理下标,不是逻辑下标,调用方必须先自己换算;animate为false时走css(),用于初始化定位和无缝复位;done是回调,index的更新都放在这里。
stop(true, true)是防御性的:清掉队列里没跑完的动画并直接跳到终点。因为外面有animating锁,理论上不会有两个动画并存,但用户快速点击、或者浏览器掉帧导致回调延迟时,队列里偶尔会残留一个,加上这一句能避免「滑到一半停住」。
提示:
.animate()动画的是left,属于布局属性,每帧都会触发重排。图片多、动画频繁时会有掉帧感。要更顺滑,可以把left换成transform: translate3d(),用 CSStransition做动画、用transitionend收尾,代价是不能再复用 jQuery 的.animate()。
4.3 next / prev / goTo 三个动作函数
三个函数结构几乎一样,差别只在边界判断:
Carousel.prototype.next = function () { if (this.animating) return; // 锁:动画期间不接受新指令 this.animating = true; var self = this; var isLast = this.index === this.length - 1; // 物理下标:真实第 i 张在 i+1;最后一张的下一张是克隆首张,即 length+1 var physical = isLast ? this.length + 1 : this.index + 2; this._moveTo(physical, true, function () { if (isLast) { self.index = 0; self._moveTo(1, false); // 瞬间复位到真实首张 } else { self.index += 1; } self.animating = false; self._syncDots(); }); }; Carousel.prototype.prev = function () { if (this.animating) return; this.animating = true; var self = this; var isFirst = this.index === 0; // 真实第 i 张物理下标是 i+1,前一张就是 i var physical = isFirst ? 0 : this.index; this._moveTo(physical, true, function () { if (isFirst) { self.index = self.length - 1; self._moveTo(self.length, false); // 复位到真实末张 } else { self.index -= 1; } self.animating = false; self._syncDots(); }); }; Carousel.prototype.goTo = function (target) { // 点圆点:目标就是逻辑下标,跨张切换直接动画过去,不做无缝处理 if (this.animating || target === this.index) return; this.animating = true; var self = this; this._moveTo(target + 1, true, function () { self.index = target; self.animating = false; self._syncDots(); }); };goTo和next的处理方式不同,这点值得说一下:点圆点是从第 1 张直接跳到第 4 张,用户预期就是「唰」地滑过去,不需要无缝,所以直接目标定位;而next是逐步前进,走到边界才需要克隆张兜底。如果你的产品要求点圆点也走「最短路径」(比如从第 4 张点第 1 张,判断是往左滑近还是往右滑近),那goTo里就要加一层方向判断,这是二次开发里常见的增强点。
_syncDots只有一行,但它是画面与状态之间唯一的同步点:
Carousel.prototype._syncDots = function () { this.$dots.removeClass('active').eq(this.index).addClass('active'); };4.4 事件命名空间与 destroy
jQuery 的事件命名空间是这套代码里第二值得学的技巧。所有绑定都带.carousel后缀,销毁时一句off('.carousel')全部摘掉:
Carousel.prototype._bind = function () { var self = this; this.$next.on('click.carousel', function () { self.next(); }); this.$prev.on('click.carousel', function () { self.prev(); }); this.$dots.on('click.carousel', function () { self.goTo($(this).index()); // 圆点的顺序 = 逻辑下标 }); if (this.opts.pauseOnHover) { this.$root.on('mouseenter.carousel', function () { self.pause(); }) .on('mouseleave.carousel', function () { self.play(); }); } $(document).on('visibilitychange.carousel', function () { document.hidden ? self.pause() : self.play(); }); // 窗口尺寸变化:重算宽度并重新定位,避免移动端横竖屏切换后错位 $(window).on('resize.carousel', function () { self.itemWidth = self.$root.width(); self.$list.css('width', (self.length + (self.opts.seamless ? 2 : 0)) * self.itemWidth); self._moveTo(self.index + (self.opts.seamless ? 1 : 0), false); }); }; Carousel.prototype.destroy = function () { this.pause(); $(document).off('.carousel'); $(window).off('.carousel'); this.$root.find('*').addBack().off('.carousel'); this.$list.find('.clone').remove(); // 克隆节点必须清掉,否则重复初始化会翻倍 };destroy里最后一行容易被漏掉。单页应用里组件反复挂载卸载,克隆节点如果不清,第二次初始化会再补两个,轨道宽度算出来是错的,表现是「刷新几次之后轮播图越来越窄」。用户点击圆点时用$(this).index()取下标,前提是圆点数量与真实张数一致——所以圆点要么在 HTML 里写死并对齐,要么在init里按length动态生成,二选一,别混着来。
4.5 移动端滑动支持
PC 上调通了,移动端大概率还要能划。加一段触摸逻辑就够,注意横向滑动时要阻止页面上下滚:
Carousel.prototype._bindTouch = function () { var self = this, startX = 0, startY = 0, moved = false; this.$root.on('touchstart.carousel', function (e) { var t = e.originalEvent.touches[0]; startX = t.clientX; startY = t.clientY; moved = false; self.pause(); // 手指按下就停自动播放 }); this.$root.on('touchmove.carousel', function (e) { var t = e.originalEvent.touches[0]; // 横向位移大于纵向,判定为划动轮播,阻止默认的页面滚动 if (Math.abs(t.clientX - startX) > Math.abs(t.clientY - startY)) { e.preventDefault(); moved = true; } }); this.$root.on('touchend.carousel', function (e) { if (!moved) { self.play(); return; } // 只是点了一下,不算划动 var delta = e.originalEvent.changedTouches[0].clientX - startX; if (delta <= -30) self.next(); // 左划,阈值 30px 防止误触 else if (delta >= 30) self.prev(); // 右划 self.play(); // 无论划没划成,都恢复自动播放 }); };阈值 30px 是经验值,太小会误触,太大用户觉得划不动。moved标记的作用是区分「点击」和「划动」——如果用户只是轻点了一下图片,不应该触发切换,但手指按下时已经pause()了,所以touchend里必须把play()补回来,否则自动播放就永久停了。这个细节漏掉的话,移动端表现就是「点一下图,轮播就不动了」,属于典型的不报错但功能缺失。
5. 避坑与排查:无缝轮播最容易翻车的五个地方
轮播图的 bug 有个共同特点:不报错,只是「看起来不对」。控制台干干净净,画面就是不对,所以得靠现象反推原因。下面五条是我在轮播图上踩过或者帮别人看过的,按「现象 → 原因 → 解决」记下来。
5.1 首屏闪一下,或者从最后一张回到第一张时有明显回闪
现象:页面加载完,第一张图闪了一下才正常;或者每次从第 4 张切到第 1 张,能看到画面往右「倒着滑」一小段。
原因:通常有两种。一是初始化时先渲染了未克隆的 DOM,浏览器已经把 4 张图铺开了,JS 才补上克隆并定位,中间那一下就是闪烁;二是复位用的_moveTo(1, false)写成了_moveTo(1, true),于是「无缝复位」变成了「有动画地倒回去」,正好露馅。
解决:容器在 CSS 里就加overflow: hidden,让未初始化时的溢出不可见;初始化脚本放在</body>前或者$(function(){})里,保证 DOM 已就绪。复位必须是animate = false,走css()直接定位,不产生中间帧。如果还是闪,检查克隆节点的clone()是否丢了addClass('clone'),导致样式没有继承。
5.2 快速连点箭头,图片错位或者直接卡住不动
现象:手速快的时候连点五下「下一张」,画面停在第 3 张,圆点却亮在第 5 个;再点一下就彻底不动了。
原因:没加动画锁,或者锁加了但没在所有出口释放。animating = true之后,如果动画回调里因为异常提前return,锁就永远不释放,之后所有点击都被if (this.animating) return;挡掉,表现就是「卡死」。另一种情况是stop(true, true)把动画直接跳到终点但不触发complete,回调没跑,index没更新。
解决:确保animating = false写在回调的最外层,不要放在if分支里面;stop()的第一个参数用true(清队列)、第二个用true(跳到终点),让.animate的回调一定会执行。调试时在next()第一行打个console.log(this.animating, this.index),连点几次就能看出锁有没有正常释放。
5.3 鼠标移出之后,轮播越转越快
现象:在图上反复移入移出几次,之后自动播放的间隔明显变短,越移越快。
原因:play()里没有先pause(),mouseleave每次都新挂一个setInterval,旧的一直没清。移入三次移出三次,就有三个定时器同时在跑,每个都调next(),效果就是三倍速。
解决:play()开头强制this.pause(),这是唯一正确的写法,不要指望调用方保证顺序。另外可以在pause()里把this.timer置null,方便在控制台console.log(inst.timer)确认是否真的清干净了。
5.4 hover 暂停后不再恢复,或者切到后台回来疯狂连跳
现象:鼠标移进图里停住了,移出来还是不动;或者切到别的标签页几秒再切回来,图片「唰唰唰」连跳好几张。
原因:前者一般是mouseleave绑定丢了——注意mouseenter/mouseleave和mouseover/mouseout的区别,后者会在子元素之间冒泡,鼠标从图片移到箭头上就触发一次mouseout,pause()被调用两次、play()只调用一次,状态就歪了。后者是标签页后台被节流,setInterval的触发积压,切回来集中执行。
解决:统一用mouseenter/mouseleave,它们不冒泡,一个容器只触发一次。后台场景挂visibilitychange,隐藏时pause()、显示时play(),配合play()内部的pause(),积压问题一次解决。
5.5 换个屏幕宽度就错位,图只露一半
现象:在 1920 宽的屏幕上正常,缩小窗口之后每张图右边被切掉一截,或者右边的空白越来越大。
原因:itemWidth在构造时取了一次$root.width()就再也没更新,而容器宽度是响应式的(width: 100%)。轨道总宽度按旧值算,li的宽度却是 CSS 里的新值,两边对不上。
解决:监听resize,重算itemWidth、重写轨道width、并按当前index重新无动画定位。更省事的做法是把容器宽度写死成设计稿尺寸,外层再用transform: scale()整体缩放,计算逻辑完全不用改。还有一种情况是li上写了百分比宽度,那itemWidth就得改成this.$items.eq(0).outerWidth(),不能再用容器宽度。
6. 二次开发:把 demo 改成能进项目的轮播组件
demo 跑通之后,真正的工作才刚开始——它现在只能用一次,改个参数要动源码。第一步是把它封成插件,让页面上能挂多个实例、每个实例各有一套配置:
$.fn.carousel = function (options) { return this.each(function () { var $this = $(this); var inst = $this.data('carousel'); if (!inst) { inst = new Carousel($this, $.isPlainObject(options) ? options : {}); $this.data('carousel', inst); // 实例挂在 data 上,外部可取 } // 支持 $('#banner').carousel('next') 这种命令式调用 if (typeof options === 'string' && typeof inst[options] === 'function') { inst[options](); } }); }; // 使用:一个页面挂两个,参数互不影响 $(function () { $('#banner').carousel({ speed: 400, interval: 3000, seamless: true }); $('#sidebar').carousel({ speed: 250, interval: 5000, autoplay: false }); });剩下几个改造点按优先级排,我一般的顺序是这样:
| 改造点 | 具体做法 | 收益 |
|---|---|---|
| 圆点动态生成 | init里按length拼span,不再手写 HTML | 改图不用改结构,少一处出错点 |
| 图片懒加载 | 只给当前和下一张设src,其余放style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;" />
|