深入解析js轮播插件核心代码的实现过程


Posted in Javascript onApril 14, 2017

轮播效果在网页中用的很多,swiper是其中最有代表性的作品,它支持水平和竖直滑动,还有反弹效果,兼容移动端和pc端。当然代码量也是相当大的,单是js就有5300行(3.4.0的未缩版本),若不考虑代码利用率和加载速度直接就用了,在移动端比较慎重,比如京东(m.jd.com)的轮播就没有用它,而是自己实现了类似的功能,代码量很少的样子(格式化之后看起来二三百行左右的样子)。那么这个功能如果自己来实现,要怎么做呢?

准备工作

1. 准备几张图片(我这里放了四张)

2. 搭建目录结构(html+css+images+js)

3. 编写代码,实现初始状态的显示效果

<!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" rel="external nofollow" >
  <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>

写几行样式,先让页面有一种滚动前的初始画面。

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: .3s;
  -moz-transition-duration: .3s;
  -o-transition-duration: .3s;
  transition-duration: .3s;
}
 
.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: 4rem;
  color: gray;
  -webkit-user-select:none;
  user-select:none;
}
 
.next {
  right: 0px;
}
 
.prev {
  left: 0px;
}

做好这个静态页,可以帮助我们在开发过程中预览效果,方便查找问题,欣赏制作过程带来的乐趣。

在线预览

搭建程序骨架

接下来就是写js了,先搭一个程序的架子,我这里仿一下jQ的无new式设计(其实是自己在内部自动实现new的过程)

;( 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. 向右滚动正好相反。

深入解析js轮播插件核心代码的实现过程

移动可以用css3的transform:translate3d来做,也可以用js变化left/top来做,用translate3d来做的核心代码如下:

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

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(当前显示的图片索引)的值。如果到了最右边就不能再允许滚动了。

var activeIndex = this.lastIndex--;<br>if(activeIndex > this.lastIndex){
   return;
}

同理,到了最左边,也不能继续往左滚动了(听起来是一句非常正确的废话)

var activeIndex = --this.activeIndex;
if(activeIndex < this.firstIndex){
return;
}

现在要考虑自动滚动的情况,如果是自动轮播的情况,那就不能直接return; 要么当到达最右边的时候,activeIndex赋成firstIndex,当达到最左边的时候,activeIndex赋成lastIndex; 这样做的实际效果看起来就是像荡秋千一样,这显然不是我们想要的效果。要么跳到最前面重新开始,这做出来的实际效果一点也不连续。最后决定去看看swiper是怎么实现的。swiper的做法就是把第一张复制一份放到最后面,把最后一张复制一份插到最前面。 如下图所示:

深入解析js轮播插件核心代码的实现过程

第一张和第四张都被复制了一份。对应的代码如下:

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就可以了。关键代码如下:

transition(this.wrap,0);

不过这一步要在从4变为1,刚停下来的时候,立即执行。做早了,就会感觉是直接从4跳到1,没有动画效果,做晚了,就会出现空白,并因为索引溢出而报错。所以这里需要一个修复方法:

fn.fixedPrevLoop = function(){
    var that = this;
    setTimeout(function(){
      that.fixedLoop(that.lastIndex-1)
    },that.params.speed);
  }

或者:

this.container.addEventListener('transitionend',function(){<br>   

//监听动画结速之后再执行判断是否要修复<br>},false);

做完这一步,就看起来有连续滚动的效果了,在这个基础上实现自动轮播就好办了,只要用定时器,每隔一段时间就执行一下自身就可以了。代码如下:

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()方法。不过我这里没有这样做,因为考虑到后面我们还要做手势(鼠标)拖动翻页效果,我决定用事件代理来做。让事件统统都冒泡到包装容器上去处理,这也是提升性能的常用技巧之一。

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方法,可以被监听函数自动捕获到,这个函数名是固定的,不明白的可以自行搜索这个函数名。

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对应的页就算是反弹了。代码就是这样:

fn.stop = function(){
    this.axis.x = 0;
    translate3d(this.wrap,this.slideWidth*this.activeIndex);
    transition(this.wrap,this.params.speed);
}

接下来就是做页码指示器了,这个简单,翻页成功之后就更新一下对应的小点就是了。由于我们人为的插了两个页面进去,索引数和页码数就对应不起来了,实际参与滚动的图片有6张,但是只能显示4个点。我做的时候走了一些弯路,现在总结起来无非就是两点:

1. 给小?A点加一个属性用来标记是哪个页面。用于处理点击滚动的时候,知道是跳到哪个页面。

2. 用一个数组来保存页面索引,比如【3,0,1,2,3,1】,这样当自动或拖动翻页的时候,可以通过activeIndex的值,确定要高亮哪个页码指示器。(小圆点)

也可能还有更好的方法,暂时就先这样实现吧。先把功能做出来,后面有时间,有灵感了再去优化。

到这里,几本上就做完了,只是还要再完善一些边际情况,比如一张图都没有的情况,默认参数的处理等。

思考

除了这个方法之外是不是有其它解决方法呢?比如下图这样,红框中只放三张,多余的叠在屏后面,移动的时候不是移红框,而是真实的移动图片?

深入解析js轮播插件核心代码的实现过程

 这种方法也是可以行的通的,不过我在尝试的时候,发现这种方法在手机上有些卡顿,在电脑上看,容器边框会有闪动,原因没有深入去查了。

  咳!咳..,下载源码请上号称全球最大的同性交友网站github.com  ,在线预览无图片版本

小结一下

写到这里的时候,主体的逻辑差不多就实现了, 只用了大约三百多行代码,虽然很精简,但是这里还有许多东西没有考虑进去,比如竖直方向的滚动,兼容性问题等等。通过画草图的方法来帮助理清思路,遇到困难,可以借鉴别人的实现方法,参考但不是原封不动的复制粘贴。代码组织要考虑扩展性和可读性,先从程序的骨架写起,然后再去写方法,处理细节问题。通过动手实践,可以发现一些看似简单的东西,做起来也不是那么容易的。自己做出来之后,再去看别人写的好的代码,就会知道人家哪些地方比我实现的好,有哪些值得学习的地方。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
超轻量级的基于jquery的三级展开列表
Apr 26 Javascript
jquery toolbar与网页浮动工具条具体实现代码
Jan 12 Javascript
js控制当再次点击按钮时的间隔时间
Jun 03 Javascript
DOM 事件流详解
Jan 20 Javascript
Javascript递归打印Document层次关系实例分析
May 15 Javascript
一分钟理解js闭包
May 04 Javascript
Angularjs 创建可复用组件实例代码
Oct 09 Javascript
jQuery模拟淘宝购物车功能
Feb 27 Javascript
JavaScript ES6中const、let与var的对比详解
Jun 18 Javascript
vue给input file绑定函数获取当前上传的对象完美实现方法
Dec 15 Javascript
微信小程序渲染性能调优小结
Jul 30 Javascript
Javascript中的解构赋值语法详解
Apr 02 Javascript
基于JavaScript实现的希尔排序算法分析
Apr 14 #Javascript
Vue2.0 UI框架ElementUI使用方法详解
Apr 14 #Javascript
iOS + node.js使用Socket.IO框架进行实时通信示例
Apr 14 #Javascript
JavaScript Canvas绘制圆形时钟效果
Aug 20 #Javascript
基于JavaScript实现的插入排序算法分析
Apr 14 #Javascript
基于JavaScript实现的折半查找算法示例
Apr 14 #Javascript
AngularJS之自定义服务详解(factory、service、provider)
Apr 14 #Javascript
You might like
推荐一篇入门级的Class文章
2007/03/19 PHP
PHP超低内存遍历目录文件和读取超大文件的方法
2019/05/01 PHP
laravel利用中间件防止未登录用户直接访问后台的方法
2019/09/30 PHP
syntaxhighlighter 使用方法
2007/07/02 Javascript
浅谈Javascript事件处理程序的几种方式
2012/06/27 Javascript
动态获取复选框checkbox选中个数的jquery代码
2013/06/25 Javascript
js动画效果制件让图片组成动画代码分享
2014/01/14 Javascript
页面get请求 中文参数方法乱码问题的快速解决方法
2016/05/31 Javascript
JavaScript中setTimeout的那些事儿
2016/11/14 Javascript
对比分析Django的Q查询及AngularJS的Datatables分页插件
2017/02/07 Javascript
React-intl 实现多语言的示例代码
2017/11/03 Javascript
解决Layui数据表格中checkbox位置不居中的方法
2018/08/15 Javascript
vue在自定义组件中使用v-model进行数据绑定的方法
2019/03/25 Javascript
AngularJS实现的鼠标拖动画矩形框示例【可兼容IE8】
2019/05/17 Javascript
Vue实现商品详情页的评价列表功能
2019/09/04 Javascript
[05:31]DOTA2英雄梦之声_第08期_莉娜
2014/06/23 DOTA
[11:33]DAC2018 4.5SOLO赛决赛 MidOne vs Paparazi第二场
2018/04/06 DOTA
python 算法 排序实现快速排序
2012/06/05 Python
Python多线程编程(六):可重入锁RLock
2015/04/05 Python
Python3中的列表生成式、生成器与迭代器实例详解
2018/06/11 Python
python 字典 setdefault()和get()方法比较详解
2019/08/07 Python
使用Python实现Wake On Lan远程开机功能
2020/01/22 Python
python map比for循环快在哪
2020/09/21 Python
美国休闲服装品牌:J.Crew Factory
2017/03/04 全球购物
英国足球店:UK Soccer Shop
2017/11/19 全球购物
怎么处理XML的中文问题
2015/03/26 面试题
什么是数据抽象
2016/11/26 面试题
EJB需直接实现它的业务接口或Home接口吗,请简述理由
2016/11/23 面试题
学习委员自我鉴定
2014/01/13 职场文书
统计岗位职责
2014/02/21 职场文书
学校运动会报道稿
2014/09/23 职场文书
2014年学习部工作总结
2014/11/12 职场文书
集团财务总监岗位职责
2015/04/03 职场文书
生日赠语
2015/06/23 职场文书
情况说明书格式及范文
2019/06/24 职场文书
Redis keys命令的具体使用
2022/06/05 Redis