300行代码揭密轮播插件核心代码的实现过程
轮播效果在网页中用的很多,swiper是其中最有代表性的作品,它支持水平和竖直滑动,还有反弹效果,兼容移动端和pc端。当然代码量也是相当大的,单是js就有5300行(3.4.0的未缩版本),若不考虑代码利用率和加载速度直接就用了,在移动端比较慎重,比如京东(m.jd.com)的轮播就没有用它,而是自己实现了类似的功能,代码量很少的样子(格式化之后看起来二三百行左右的样子)。那么这个功能如果自己来实现,要怎么做呢?
准备工作
1. 准备几张图片(我这里放了四张)
2. 搭建目录结构(html+css+images+js)
3. 编写代码,实现初始状态的显示效果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | <! DOCTYPE html> < html > < head > < meta charset="utf-8"> < meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" /> < title >轮播</ title > < link rel="stylesheet" type="text/css" href="slider.css"> < script src="slider.js"></ script > </ head > < body > < div id="slider" class="slider-wrapper"> < ul class="slider-items"> < li class="slider-item">< img src="images/pic21.gif" alt="1"></ li > < li class="slider-item">< img src="images/pic22.gif" alt="2"></ li > < li class="slider-item">< img src="images/pic23.gif" alt="3"></ li > < li class="slider-item">< img src="images/pic24.gif" alt="4"></ li > </ ul > </ div > < script > Slider('#slider',{}); </ script > </ body > </ html > |
写几行样式,先让页面有一种滚动前的初始画面。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 | body { padding : 0 ; min-width : 300px ; max-width : 640px ; margin : 0 auto ; } ul { list-style : none ; } ul,li { margin : 0 ; padding : 0 ; } .slider-wrapper { position : relative ; width : 100% ; height : 220px ; overflow : hidden ; -webkit-user-select: none ; -ms-user-select: none ; user-select: none ; } .slider-items { position : relative ; height : 100% ; } .slider-item { float : left ; text-align : center ; cursor : pointer ; } .slider-item > img { width : 100% ; pointer-events: none ; } .slider-pagination { position : absolute ; bottom : 10px ; left : 0 ; width : 100% ; text-align : center ; -webkit-transition-duration: . 3 s; -moz-transition-duration: . 3 s; -o-transition-duration: . 3 s; transition-duration: . 3 s; } .slider-bullet { width : 8px ; height : 8px ; margin : 0 5px ; display : inline- block ; border-radius: 100% ; background-color : black ; opacity: . 2 ; cursor : pointer ; } .slider-bullet-active { opacity: 1 ; background-color : #007aff ; } .slider-button { position : absolute ; top : 50% ; width : 50px ; height : 50px ; text-align : center ; line-height : 50px ; margin-top : -25px ; z-index : 10 ; font-size : 4 rem; color : gray ; -webkit-user-select: none ; user-select: none ; } .next { right : 0px ; } .prev { left : 0px ; } |
做好这个静态页,可以帮助我们在开发过程中预览效果,方便查找问题,欣赏制作过程带来的乐趣。
搭建程序骨架
接下来就是写js了,先搭一个程序的架子,我这里仿一下jQ的无new式设计(其实是自己在内部自动实现new的过程)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | ;( function ( global, factory ) { "use strict" ; if ( typeof module === "object" && typeof module.exports === "object" ) { module.exports = global.document ? factory( global, true ) : function ( w ) { if ( !w.document ) { throw new Error( "Slider requires a window with a document" ); } return factory( w ); }; } else { factory( global ); } // Pass this if window is not defined yet } )( typeof window !== "undefined" ? window : this , function ( window, noGlobal ){ "use strict" ; function Slider( selector, options ) { return new Slider.init( selector, options ); } Slider.init= function (selector, params){ next: function (){},<br> prev: function (){},<br> move: function (){} }<br> Slider.init.prototype = Slider.prototype = {<br> <br> }; return Slider }); |
架子搭好之后,先停下来喝口水,思考一下,最终我们的这个插件要实现哪些功能。
1. 点击左、右箭头可以控制滚动
3. 可以向左、向右拖拽滑动
4. 返弹效果
5. 自动轮播效果
6. 页码指示
这些功能该怎么实现?
先画几张草图,在纸上模拟一下这个过程,搞明白之后,再用代码来让计算机执行。下图中的蓝框代表显示器的宽度,红框代表图片所在容器的实际宽度。我只要移动红框,那么在屏幕上看起来,就会有轮播滚动的效果。原理看起来是不是很简单?本着先易后难,循序渐进的作战方针,先不考虑循环滚动。假设屏幕上显示的是图片1,此时只要把红框往左移一个屏的宽度,那么就会显示2,再移一屏,就可以显示3. 向右滚动正好相反。
移动可以用css3的transform:translate3d来做,也可以用js变化left/top来做,用translate3d来做的核心代码如下:
1 2 3 4 5 6 7 8 9 10 11 | function translate3d(element,x,y) { x = x === undefined ? 0 : x; y = y === undefined ? 0 : x; element.style[ '-webkit-transform' ] = 'translate3d(-' +x+ 'px,' +y+ 'px,0px)' ; element.style[ 'transform' ] = 'translate3d(-' +x+ 'px,' +y+ 'px,0px)' ; } function transition(element,time){ element.style[ '-webkit-transition-duration' ] = time+ 'ms' ; element.style[ 'transition-duration' ] = time+ 'ms' ; } |
x控制左右移动,y控制竖直移动(本例中暂不考虑),z固定(0px). 当x移动的距离是正数的时候,向左滚动--prev,为负数的时候向右滚动--next
1 2 3 4 5 6 7 8 9 10 11 | fn.next = function (){ var activeIndex = ++ this .activeIndex; translate3d( this .wrap,activeIndex* this .slideWidth); transition( this .wrap, this .params.speed); } fn.prev = function (){ var activeIndex = -- this .activeIndex; translate3d( this .wrap,activeIndex* this .slideWidth); transition( this .wrap, this .params.speed); } |
由于图片数量是有限的,不能一直滚动,如果到头了,需要做一些处理。因此需要判断activeIndex(当前显示的图片索引)的值。如果到了最右边就不能再允许滚动了。
1 2 3 | var activeIndex = this .lastIndex--;<br> if (activeIndex > this .lastIndex){ return ; } |
同理,到了最左边,也不能继续往左滚动了(听起来是一句非常正确的废话)
1 2 3 4 | var activeIndex = -- this .activeIndex; if (activeIndex < this .firstIndex){ return ; } |
现在要考虑自动滚动的情况,如果是自动轮播的情况,那就不能直接return; 要么当到达最右边的时候,activeIndex赋成firstIndex,当达到最左边的时候,activeIndex赋成lastIndex; 这样做的实际效果看起来就是像荡秋千一样,这显然不是我们想要的效果。要么跳到最前面重新开始,这做出来的实际效果一点也不连续。最后决定去看看swiper是怎么实现的。swiper的做法就是把第一张复制一份放到最后面,把最后一张复制一份插到最前面。 如下图所示:
第一张和第四张都被复制了一份。对应的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | fn.createLoopItems = function (){ var lastItem = this .slides[ this .lastIndex]; var firstItem = this .slides[ this .firstIndex]; var prevItem = lastItem.cloneNode( true ); var nextItem = firstItem.cloneNode( true ); var sliderCount = this .sliderCount+2; var slideWidth = this .slideWidth; this .slideStack.push( this .firstIndex); this .slideStack.unshift( this .lastIndex); this .wrap.insertBefore(prevItem,firstItem); this .wrap.appendChild(nextItem); this .wrap.style.width = slideWidth * sliderCount + 'px' ; translate3d( this .wrap,slideWidth); this .activeIndex += 1; this .sliderCount = sliderCount; this .lastIndex += 2; } |
不得不承认这个做法很巧妙。随便我们往哪个方向翻,都不至于出现空白期。当翻到最未尾(数字4)的时候,还可以再翻一页,即第一张的复制品,虽然不是真的第一张,但是看起来就像是平滑的过渡到了第一张一样。不过这是临时的,我们需要在过渡完之后,立即回到真正的1的位置上去。因为我们实际上在未端只补了一张,翻完这一页,如果不进一步处理,还是会到头。这时,就是问题的关键了。当我们往右翻,从第四张翻到复制的第一张时,需要悄悄地,人不知,鬼不觉的把红框的位置移到1的真身上来。同时把activeIndex也置为图1的索引。那么问题又来了,怎么才能做到人不知鬼不觉的暗渡陈仓呢?其实很简单,只要把移位动画的时间改成0就可以了。关键代码如下:
1 | transition( this .wrap,0); |
不过这一步要在从4变为1,刚停下来的时候,立即执行。做早了,就会感觉是直接从4跳到1,没有动画效果,做晚了,就会出现空白,并因为索引溢出而报错。所以这里需要一个修复方法:
1 2 3 4 5 6 | fn.fixedPrevLoop = function (){ var that = this ; setTimeout( function (){ that.fixedLoop(that.lastIndex-1) },that.params.speed); } |
或者:
1 | this .container.addEventListener( 'transitionend' , function (){<br> //监听动画结速之后再执行判断是否要修复<br>},false); |
做完这一步,就看起来有连续滚动的效果了,在这个基础上实现自动轮播就好办了,只要用定时器,每隔一段时间就执行一下自身就可以了。代码如下:
1 2 3 4 5 6 7 8 9 10 11 | fn.autoPlay = function (){ var that = this ; if (! this .params.autoplay){ return ; } this .timeId = setTimeout( function (){ that.next(); //that.prev(); that.autoPlay(); }, this .params.delay); } |
我注释掉了prev(), 默认都是向右自动轮播。如果要改成是往左轮播,只要在这里加一个一个配置选择就好了。自动循环都做好了,在此基础上点击翻页,也是很容易的事情了,给按钮邦定一个click事件,如果是右边的,就调用next()方法,反之则调用prev()方法。不过我这里没有这样做,因为考虑到后面我们还要做手势(鼠标)拖动翻页效果,我决定用事件代理来做。让事件统统都冒泡到包装容器上去处理,这也是提升性能的常用技巧之一。
1 2 3 4 5 6 7 8 9 10 11 12 13 | fn.bindEvents = function (){ if (Device.desktop){ this .container.addEventListener( 'mousedown' , this , false ); this .container.addEventListener( 'mousemove' , this , false ); document.addEventListener( 'mouseup' , this , false ); } else { this .container.addEventListener( 'touchstart' , this , false ); this .container.addEventListener( 'touchmove' , this , false ); document.addEventListener( 'touchend' , this , false ); } this .container.addEventListener( 'transitionend' , this , false ); this .container.addEventListener( 'click' , this , false ); } |
为什么这里的addEventListener为什么是邦定this而不是一个函数呢?简单说,是因为上下文中有一个handleEvent方法,可以被监听函数自动捕获到,这个函数名是固定的,不明白的可以自行搜索这个函数名。
1 2 | fn.handleEvent = function (e){ var type = e.type;<br> //注意这里边的this<br>} |
这样做的好处是可以维持事件邦定的那个函数的上下文。简单说就是不用操心this的指向会变。
做完这一步,就可以做拖动翻页了。在pc上用鼠标,在手机上用手指,处理的方式都是一样的,监听按下,移动,释放这三个事件。按下的时候,记住初始坐标,移动的时候和这个坐标进行对比,计算出移动的距离,然后更新到移动的对象上(红框)。这里边有几个地方需要注意:
1. 如果移动的时候,不是直线,x坐标和y坐标都有改变,是判断成水平拖动还是垂直拖动?
2. 在pc上,如何判断是拖动,拖出屏幕外了怎么处理?
3. 反弹怎么做?
对于第1点,可以比较两个方向距离的大小,谁大听谁的。如果指向了是水平滚动,那么可以直接忽略竖直方向的变化。
对于第2点,可以把监听mouseup放到document上去,最好加一个移动的距离大小判断,如果超过容器的大小,就当作是释放了,该反弹的反弹,该滑页的滑页。
对于第3点,在拖动释放的时候,判断移动距离,比如拖动的距离小于屏宽的1/3,就反方向translate相应的距离回去,甚至都不用关心这个距离,反正这时的activeIndex没有更新的,直接回到这个activeIndex对应的页就算是反弹了。代码就是这样:
1 2 3 4 5 | fn.stop = function (){ this .axis.x = 0; translate3d( this .wrap, this .slideWidth* this .activeIndex); transition( this .wrap, this .params.speed); } |
接下来就是做页码指示器了,这个简单,翻页成功之后就更新一下对应的小点就是了。由于我们人为的插了两个页面进去,索引数和页码数就对应不起来了,实际参与滚动的图片有6张,但是只能显示4个点。我做的时候走了一些弯路,现在总结起来无非就是两点:
1. 给小圓点加一个属性用来标记是哪个页面。用于处理点击滚动的时候,知道是跳到哪个页面。
2. 用一个数组来保存页面索引,比如【3,0,1,2,3,1】,这样当自动或拖动翻页的时候,可以通过activeIndex的值,确定要高亮哪个页码指示器。(小圆点)
也可能还有更好的方法,暂时就先这样实现吧。先把功能做出来,后面有时间,有灵感了再去优化。
到这里,几本上就做完了,只是还要再完善一些边际情况,比如一张图都没有的情况,默认参数的处理等。
思考
除了这个方法之外是不是有其它解决方法呢?比如下图这样,红框中只放三张,多余的叠在屏后面,移动的时候不是移红框,而是真实的移动图片?
这种方法也是可以行的通的,不过我在尝试的时候,发现这种方法在手机上有些卡顿,在电脑上看,容器边框会有闪动,原因没有深入去查了。
咳!咳..,下载源码请上号称全球最大的同性交友网站github.com , 在线预览无图片版本
写到这里的时候,主体的逻辑差不多就实现了, 只用了大约三百多行代码,虽然很精简,但是这里还有许多东西没有考虑进去,比如竖直方向的滚动,兼容性问题等等。通过画草图的方法来帮助理清思路,遇到困难,可以借鉴别人的实现方法,参考但不是原封不动的复制粘贴。代码组织要考虑扩展性和可读性,先从程序的骨架写起,然后再去写方法,处理细节问题。通过动手实践,可以发现一些看似简单的东西,做起来也不是那么容易的。自己做出来之后,再去看别人写的好的代码,就会知道人家哪些地方比我实现的好,有哪些值得学习的地方。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· 展开说说关于C#中ORM框架的用法!
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?