深入解析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 相关文章推荐
Javascript图片上传前的本地预览实例
Jun 16 Javascript
js操作IE浏览器弹出浏览文件夹可以返回目录路径
Jul 14 Javascript
jQuery在页面加载时动态修改图片尺寸的方法
Mar 20 Javascript
JavaScript中SetInterval与setTimeout的用法详解
Nov 10 Javascript
Angular-Touch库用法示例
Dec 22 Javascript
Vue开发中整合axios的文件整理
Apr 29 Javascript
详解如何在vue项目中引入elementUI组件
Feb 11 Javascript
vue项目tween方法实现返回顶部的示例代码
Mar 02 Javascript
vue数据控制视图源码解析
Mar 28 Javascript
vue中上传视频或图片或图片和文字一起到后端的解决方法
Dec 01 Javascript
vue 对axios get pust put delete封装的实例代码
Jan 05 Javascript
基于javascript原生判断DOM是否加载完毕
Oct 14 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
PHP实现仿Google分页效果的分页函数
2015/07/29 PHP
php 无限级分类 获取顶级分类ID
2016/03/13 PHP
PHP 对象继承原理与简单用法示例
2020/04/21 PHP
Jquery+WebService 校验账号是否已被注册的代码
2010/07/12 Javascript
判断用户的在线状态 onbeforeunload事件
2011/03/05 Javascript
基于jquery的无刷新分页技术
2011/06/11 Javascript
JQuery操作表格(隔行着色,高亮显示,筛选数据)
2012/02/23 Javascript
如何调试异步加载页面里包含的js文件
2014/10/30 Javascript
node.js超时timeout详解
2014/11/26 Javascript
JS实现仿google、百度搜索框输入信息智能提示的实现方法
2015/04/20 Javascript
详谈javascript中的cookie
2015/06/03 Javascript
基于JavaScript实现网页倒计时自动跳转代码
2015/12/28 Javascript
JS加载器如何动态加载外部js文件
2016/05/26 Javascript
angularjs 源码解析之scope
2016/08/22 Javascript
JS实现异步上传压缩图片
2017/04/22 Javascript
JavaScript实现无穷滚动加载数据
2017/05/06 Javascript
vue主动刷新页面及列表数据删除后的刷新实例
2018/09/16 Javascript
微信小程序引用iconfont图标的方法
2018/10/22 Javascript
Vue一个案例引发的递归组件的使用详解
2018/11/15 Javascript
JS实现求5的阶乘示例
2019/01/21 Javascript
Node.js 进程平滑离场剖析小结
2019/01/24 Javascript
TypeScript中使用getElementXXX()的示例代码
2019/09/12 Javascript
Python os模块中的isfile()和isdir()函数均返回false问题解决方法
2015/02/04 Python
Python 在 VSCode 中使用 IPython Kernel 的方法详解
2020/09/05 Python
python如何使用腾讯云发送短信
2020/09/17 Python
python MD5加密的示例
2020/10/19 Python
localstorage和sessionstorage使用记录(推荐)
2017/05/23 HTML / CSS
唤醒头发毛囊的秘密武器:Grow Gorgeous
2016/08/28 全球购物
无谷物狗粮:Pooch & Mutt
2018/05/23 全球购物
波兰快递服务:Globkurier.pl
2019/11/08 全球购物
三年级语文教学反思
2014/02/01 职场文书
青年文明号创建承诺
2014/03/31 职场文书
投诉信范文
2015/07/02 职场文书
Python实现socket库网络通信套接字
2021/06/04 Python
Java SSH 秘钥连接mysql数据库的方法
2021/06/28 Java/Android
【海涛七七解说】DCG第二周:DK VS 天禄
2022/04/01 DOTA