unslider 源码分析

1,027 阅读9分钟
原文链接: segmentfault.com

根据Bootstrap中文网的介绍,Unslider一个超小的 jQuery轮播(slider)插件,参照这个汉化版的介绍页面,这个插件有你需要的优点,但是本文是抱着学习的态度,学习如何实现轮播插件,所以有些细节可能有所忽略。

1. 如何使用

参照Bootstrap中文网提供的介绍页面,或者参照官网的介绍都是可以,虽然unslider已经升级了版本,但是使用方式(API接口)还是没有改变。

对于HTML结构的要求只需要提供类似以下结构即可:

然后引入jquery.jsunslider.js两个文件,即可以在DOM加载完执行

$(function() {
    $('.banner').unslider();
});

我取汉化版介绍页面的元素,使用最新版的unslider.js,调用unslider(),比较页面元素有什么变化。

源代码

使用插件后的效果(有所节省)

  1. 1
  2. 2
  3. 3
  4. 4

可以发现使用插件后,会在.banner上封装

,并且对.banner设置样式不让子元素溢出;在ul上设置宽度是li元素的整数倍,li元素的所有兄弟元素平均结果(100/4);还加上nextprev元素,加上了nav导航。

ul是相对于.banner定位的,虽然宽度是大于100%,但是.banner是不会被ul撑开的;而在ul上配置widthleft参数,可以控制显示ul的起始位置,left:-100%相当于ul向左飘过去了100%,通俗点说:

父元素.banner只能让ul显示一个身位,但是ul膨胀了,实际它有4个身位,相对于.banner定位,默认left:0%时,
相当于显示0-1身位的ul,为了显示第二个身位的ul,就必须将ul往左移,让它显示1-2位置的ul的,所以此时设置left: -100%

以此类推。

2. 使用 $.fn.unslider 方法

$.fn.unslider方法是在jQuery原型链定义的方法,jQuery对象自然能够调用这个方法。前面的例子中我们是直接调用的,并没有传入参数,事实上$.fn.unslider还可以接收类似这样的参数:$(".banner").unslider("fn:arg1,arg2")。最终调用在某个位置定义的fn函数,参数是arg1arg2

2.1 分析 $.fn.unslider 源码

//  And set up our jQuery plugin
$.fn.unslider = function(opts) {
  return this.each(function() {
    var $this = $(this);

    //  Allow usage of .unslider('function_name')
    //  as well as using .data('unslider') to access the
    //  main Unslider object
    if(typeof opts === 'string' && $this.data('unslider')) {
      opts = opts.split(':');

      var call = $this.data('unslider')[opts[0]];

      //  Do we have arguments to pass to the string-function?
      if($.isFunction(call)) {
        return call.apply($this, opts[1] ? opts[1].split(',') : null);
      }
    }

    return $this.data('unslider', new $.Unslider($this, opts));
  });
};

$.fn.unslider的重要逻辑都是在$.Unslider中实现的,第一次调用$.fn.unslider方法时将调用jQuery.data方法将新构造的$.Unslider实例保存到jQuery对象的缓存对象上,供后续使用;后续的调用可以直接从这个jQuery缓存对象取出$.Unslider实例调用相关方法。这样做的好处就是不会多执行$.Unslider构造方法?(好像是我自己编出来的一个理由)

jQuery插件一般最终都会在jQuery原型上定义要被jQuery对象调用的方法,或者通过直接定义的方式,如$.fn.myPlugin = function(){},或者首先定义好插件方法,然后通过$.fn.extend扩展方法将插件方法扩展到jQuery原型上。unslider插件通过了在jQuery定义静态方法$.Unslider,而$.fn.unslider只是调用入口,所有的业务逻辑都能通过$.Unslider来完成。

3. $.Unslider

首先可以把$.Unslider(context, options)看作构造函数,最终会被$.fn.unslider(options)调用。context参数是一个jQuery对象,对应要生成轮播效果的$('.banner')集合的某个元素的jQuery对象,即$($('.banner')[0]); options最终会被扩展到$.Unslider的默认参数中。

首先看$.Unslider内部对this的处理,内部会对this备份到self变量,后续的属性和方法都在self基础上定义。

$.Unslider = function(context, options) {
        var self = this;

        //  Create an Unslider reference we can use everywhere
        self._ = 'unslider';
        ...
}

我的理解,new $.Unslider的调用方法,在$.Unslider内部的this是指向$.Unslider对象自己的,如果是$('#id').Unslider()就不一样了,此时this会指向#idDOM元素,当然目前$.Unslider静态方法是无法被jQuery对象直接调用的。

3.1 $.Unslider 整体结构

整体结构:

$.Unslider = function(context, options) {
    var self = this;
    
    //插件标识
    self._ = 'unslider';
    //默认参数
    self.defaults = {
        
    };
    /**
     *  参照生成后的页面元素做个类比
     *  self.$parent => 
* self.$context => * self.$container =>
    * self.slides =>
  • */ //备份jQuery对象 self.$context = context; self.options = {}; //容器的父层 self.$parent = null; //轮播的容器jQuery,最终是self.$context的子元素的jQuery对象 //$('.banner>ul') self.$container = null; //每个轮播的页面 selft.$slides = null; //导航组 self.$nav = null; //左右指示 self.$arrows = []; //轮播页面总数 self.total = 0; //当前轮播页面的序号 self.current = 0; //前缀 self.prefix = self._ + '-'; //用于监听事件的后缀,是监听事件的命名空间 self.eventSuffix = '.' + self.prefix + ~~(Math.random() * 2e3); //定时器 self.interval = null; //初始化方法 self.init = function() { self.options = $.extend({}, self.defaults, options); //self.$container //self.$slides self.setup(); $.each(['nav', 'arrows', 'keys', 'infinite'], function(index, module) { self.options[module] && && self['init' + $._ucfirst(module)](); }); //self.initSwipe(); self.options.autoplay && self.start(); self.calculateSlides(); self.$context.trigger(self._ + '.ready'); return self.animate(self.options.index || self.current, 'init'); }; //end of self.init self.setup = function() { //css }; self.calculateSlides = function() { self.total = self.$slides.length //set total height or width }; self.start = function() { self.interval = setTimeout(function() { self.next(); }, self.options.delay); return self; }; self.stop = function() { clearTimeout(self.interval); return self; }; self.initNav = function() { }; self.initArrows = function() { }; self.initKeys = function() { }; self.initSwipe = function() { }; self.destroyArrows = function() {}; self.destroySwipe = function() {}; self.destroyKeys = function() {}; self.setIndex = function(to) { }; self.animate = function(to, dir) { }; self.next = function() { }; self.prev = function() { }; self.animateHorizontal = function(to) { }; self.animateVertical = function(to) { }; self.slide = function(prop, to) { }; self.animateFade = function() {}; self._move = function($el, obj, callback, speed) {} ; //最终调用init方法,返回self,见self.init定义 return self.init(options); };

    $.Unslider这个静态方法外,unslider插件还在jQuery原型上定义辅助方法:

    $.fn._active = function(className) {
    
    };
    
    $._ucfirst = function(str) {
    
    };
    
    $.fn._move = function() {
    
    };

    整体结构非常类似面向对象的做法,如果$.Unslider是一个类定义,而$.Unslider(context, options)就是构造函数,其他self.开头的属性和方法就是这个类的成员变量和成员方法。

    其实以_开头的方法可以理解成私有方法,unslider并不想把它暴露出去。事实上,$.Unslider的所有定义的方法都能够被外部调用,除非使用闭包的方式。

    var Unslider = (function() {
            function init(context, options) {} //初始化方法
            function _move() {}
            function next() {
                    //内部调用_move,但是整体没有暴露_move方法
                }
            var defaults = {
                
                };
            return {
                    init: init
                    next: next
                };
        })();
    $.fn.unslider = {};     
    $.fn.extend($.fn.unslider, Unslider);

    使用方式上可能就有点不同了。

    3.2 $.Unslider 源码分析

    //开始重要的源码分析

    3.2.1 默认参数

    $.Unslider = function(context, options) {
        var self = this;
    
        //  Create an Unslider reference we can use everywhere
        self._ = 'unslider';
    
        // 默认参数会被扩展到self.options
        // 最终会被外部传入的options参数覆盖,见self.init方法
        self.defaults = {
            // 是否自动开始
            autoplay: false,
            // 动画间隔微秒
            delay: 3000,
            // 速度微秒
            speed: 750,
    
            //  An easing string to use. If you're using Velocity, use a
            //  Velocity string otherwise you can use jQuery/jQ UI options.
            easing: 'swing', // [.42, 0, .58, 1],
            
            // 键盘事件相关
            keys: {
                prev: 37,
                next: 39
            },
            
            // 是否需要设置导航,设置为true在self.init方法中会调用initNav方法
            nav: true,
    
            // 上一个和下一个的指示元素
            // 默认参数扩展到self.options后
            // self.options["arrows"]可以转换为true,在self.init方法中会调用initArrows方法
            arrows: {
                prev: '',
                next: ''
            },
    
            // 方向
            animation: 'horizontal',
    
            // 选择器表达式
            selectors: {
                container: 'ul:first',
                slides: 'li'
            },
    
            //  Do you want to animate the heights of each slide as
            //  it moves
            animateHeight: false,
    
            //  Active class for the nav
            activeClass: self._ + '-active',
    
            //  Have swipe support?
            //  You can set this here with a boolean and always use
            //  initSwipe/destroySwipe later on.
            swipe: true
        };
        ...
    };

    3.2.2 init方法

    初始化方法init是由构造方法在内部调用的,最终返回这个对象self

    //  Get everything set up innit
    self.init = function(options) {
        // 扩展合并外部传入的参数和默认参数
        // 这种写法不会破坏原来的self.defaults,扩展的结果都放在{}
        self.options = $.extend({}, self.defaults, options);
    
        // 对容器进行封装,添加样式目的是让容器相对与父元素相对定位
        // 参照`unslider-wrap`这个类样式
        self.$container = self.$context.find(self.options.selectors.container).addClass(self.prefix + 'wrap');
        // 备份保存所有的轮播页面jQuery对象
        self.$slides = self.$container.children(self.options.selectors.slides);
    
        // 调用setup方法
        self.setup();
    
        // self.options合并后的选项
        // 如果存在相应的参数,且能转换为true,则调用相应的初始化方法
        $.each(['nav', 'arrows', 'keys', 'infinite'], function(index, module) {
            // $._ucfirst利用正则表达式将首字母转换为大写
            self.options[module] && self['init' + $._ucfirst(module)]();
        });
    
        // 如果引入了jquery.event.move.js和jquery.event.swipe.js文件就执行
        // 和动画相关的另外一个实现方法,与jQuery.animate同等的velocity
        if(jQuery.event.special.swipe && self.options.swipe) {
            self.initSwipe();
        }
    
        // 是否自动开始
        self.options.autoplay && self.start();
    
        // 计算
        self.calculateSlides();
    
        // 触发自定义的事件
        self.$context.trigger(self._ + '.ready');
    
        // 开始运动到指定序号的页面
        return self.animate(self.options.index || self.current, 'init');
    };

    本文中没有打算引入velocity,轮播效果最终由jQuery.animate来完成,这应该不阻碍对整个unslider插件代码的梳理分析。

    init只是初始化过程中的一个入口,它还需要其他初始化方法来帮助完成其他业务逻辑,包括setupinitNavinitArrowsinitKeysinitInfinitecalculateSlides等方法。接下来会逐个分析它们。

    3.2.3 setup方法

    self.setup = function() {
        //给轮播容器的复层(.banner)做封装
        self.$context.addClass(self.prefix + self.options.animation).wrap('
    '); //备份容器的父层,即刚才的封装层 self.$parent = self.$context.parent('.' + self._); // We need to manually check if the container is absolutely // or relatively positioned var position = self.$context.css('position'); // If we don't already have a position set, we'll // automatically set it ourselves if(position === 'static') { self.$context.css('position', 'relative'); } self.$context.css('overflow', 'hidden'); };

    setup方法主要目的是对.banner($context)做封装,设置$context的样式,如果事先没有$context的position,就设置它相对定位position:relative,设置overflow:hidden,这样只显示ul的一部分。

    3.2.4 initNav方法

    self.initNav = function() {
        // HTML5到导航标签
        var $nav = $('
      '); // 遍历轮播页面对象 self.$slides.each(function(key) { // 从元素的属性或者序号中获取 var label = this.getAttribute('data-nav') || key + 1; // 是否执行回调函数,这块不是很明白 if($.isFunction(self.options.nav)) { label = self.options.nav.call(self.$slides.eq(key), key, label); } // 增加导航项 $nav.children('ol').append('
    1. ' + label + '
    2. '); }); // 插入到$context并保存起来 self.$nav = $nav.insertAfter(self.$context); // 绑定监听事件 self.eventSuffix是命名空间,实际监听事件还是`click` self.$nav.find('li').on('click' + self.eventSuffix, function() { // Cache our link and set it to be active var $me = $(this).addClass(self.options.activeClass); // Set the right active class, remove any other ones $me.siblings().removeClass(self.options.activeClass); // 轮播到某个页面 参数是序号 self.animate($me.attr('data-slide')); }); };

      导航的这些DOM元素是在js代码中生成的,如果希望自己定制的话,可能就必须设置self.options.nav=false了,并且为导航元素绑定事件比如

      $(function() {
          var slider = $('.banner').unslider({nav: false});
          var self = slider.data('unslider');
          $('.myNav > li').each(function(key) {
                  $(this).on('click', function() {
                          var $me = $(this).addClass('activClass');
                          $me.siblings().removeClass('activeClass');
                          
                          //重要
                          self.animate(key);
                      });
              });
          });

      3.2.5 initArrows方法

      self.initArrows = function() {
          //如果指定arrows是true,则重新对self.options.arrows赋值
          //弱类型语言就是随意,啊!
          if(self.options.arrows === true) {
              self.options.arrows = self.defaults.arrows;
          }
      
          // self.defaults.arrows是默认设计好了arrows需要的元素的
          $.each(self.options.arrows, function(key, val) {
              // insertAfter返回是$(val)
              // 所以可以直接push到self.$arrows
              // self.$arrows是之前定义好的空数组
              self.$arrows.push(
                  $(val).insertAfter(self.$context).on('click' + self.eventSuffix, self[key])
              );
          });
      };

      3.2.6 initKeys方法

      self.initKeys = function() {
          //默认参数self.defaults.keys === true并不能成立
          //这里条件通过只能是外部传入的参数覆盖的
          //外部参数没有覆盖的情况,后续依然使用默认的参数
          if(self.options.keys === true) {
              self.options.keys = self.defaults.keys;
          }
      
          //使用默认参数
          $(document).on('keyup' + self.eventSuffix, function(e) {
              $.each(self.options.keys, function(key, val) {
                  if(e.which === val) {
                      $.isFunction(self[key]) && self[key].call(self);
                  }
              });
          });
      };

      按照默认的参数,最终绑定键盘事件的时候,我们看到的是

      $.each({prev: 37, next: 39}, function(key, val){
          ...
          $.isFunction(self[key]) && self[key].call(self);
      });

      最终调用的还是self.nextself.prev方法。

      3.2.7 initInfinite方法

      //  Infinite scrolling is a massive pain in the arse
      //  so we need to create a whole bloody function to set
      //  it up. Argh.
      self.initInfinite = function() {
          var pos = ['first', 'last'];
      
          $.each(pos, function(index, item) {
              self.$slides.push.apply(
                  self.$slides,
                  
                  //  Exclude all cloned slides and call .first() or .last()
                  //  depending on what `item` is.
                  self.$slides.filter(':not(".' + self._ + '-clone")')[item]()
      
                  //  Make a copy of it and identify it as a clone
                  .clone().addClass(self._ + '-clone')
      
                  //  Either insert before or after depending on whether we're
                  //  the first or last clone
                  ['insert' + (index === 0 ? 'After' : 'Before')](
                      //  Return the other element in the position array
                      //  if item = first, return "last"
                      self.$slides[pos[~~!index]]()
                  )
              );
          });
      };

      这个方法默认情况下是不会被调用的,需要在外部传入infinite参数才会被调用,如

      $(function() {
          $('.banner').unslider({infinite: true});
          });

      逐行来阅读这个方法的代码:

      1) 首先定义数组

      var pos = ['first', 'last'];

      2) 遍历数组

      $.each(pos, function(index, item) {
          
          });

      3) 向self.$slides插入克隆的轮播页面jQuery对象

      self.$slides.push.apply(
          self.$slides,
          //clone jQuery object here
      );

      首先self.$slides是在self.init方法初始化的,self.$slides = self.$container.children(self.options.selectors.slides);

      self.$slides是一个jQuery对象,为了向self.$slides插入(克隆的轮播页面的)jQuery对象,
      借用了self.$slides的方法。这里似乎是可以改成:

      self.$slides.push(
              //clone jQuery object here
          );

      有待进一步验证。

      4) 过滤获得需要克隆的元素(的jQuery对象)

      self.$slides.filter(':not(".' + self._ + '-clone")')[item]()

      其中item即为first或者last,第一次我们需要克隆第一个,第二次我们需要克隆最后一个;克隆第一个插入到self.$slides的最后位置,克隆最后一个插入到self.$slides的开头位置。如果不加过滤的话,容易导致一个问题,但是第二次克隆时通过类似self.$slides.last()方法我们获取到的是第一次克隆的结果,所以unslider利用了self._ + 'clone'类做了区分。

      5) 执行克隆并加上unslider-clone类

      .clone().addClass(self._ + '-clone')

      6) 执行插入

      ['insert' + (index === 0 ? 'After' : 'Before')](
          // relative jQuery object to insertBefor or insertAfter
      )

      首先判断是需要执行insertAfter还是insertBefore方法,接5)执行这个方法的是克隆后的jQuery对象,可以理解下面的伪代码:

      var cloneJQ; 
      cloneJQ.insertAfter(anotherJQ);

      当index === 0时,执行第一次克隆从原来self.$slides的第一个克隆插入到self.$slides结尾的位置,所以第一次应该是执行insertAfter方法。

      7) 找到相对的轮播页面jQuery对象

      self.$slides[pos[~~!index]]()

      不管是执行insertAfter还是insertBefore都是一个相对的jQuery对象;第一次克隆我们需要插入的位置是结尾,第二次插入的位置是开头。即

      index:0 --> self.$slides.last()
      index:1 --> slef.$slides.first()

      再来看pos[~~!index],这个目的是从pos数组获取某个元素,关键看~~!index的结果。举些例子:

      ~~!0    //1
      ~~!1    //0
      ~~!2    //0
      ~~!-1   //0

      这个技巧,啊!

      3.2.8 calculateSlides方法

      //  Set up the slide widths to animate with
      //  so the box doesn't float over
      self.calculateSlides = function() {
          self.total = self.$slides.length;
      
          //  Set the total width
          if(self.options.animation !== 'fade') {
              var prop = 'width';
      
              if(self.options.animation === 'vertical') {
                  prop = 'height';
              }
      
              self.$container.css(prop, (self.total * 100) + '%').addClass(self.prefix + 'carousel');
              self.$slides.css(prop, (100 / self.total) + '%');
          }
      };

      判断轮播的方向是垂直还是水平,设置容器的高度或宽度是self.$slides个数的倍数;设置每个轮播页面元素的高度或者宽度,由于是相对的,所以轮播页面的高度或宽度理论是没有改变的。

      //变化前
      ul 100px
      li 100px
      共有4个li
      
      //变化后
      ul 100 X 4 = 400 px
      li ul.width / 4 = 100 px

      另外给容器设置了unslider-carousel类,这个类的作用暂且忽略。

      3.2.9 start方法

      self.start = function() {
          self.interval = setTimeout(function() {
              //  Move on to the next slide
              self.next();
      
              //  If we've got autoplay set up
              //  we don't need to keep starting
              //  the slider from within our timeout
              //  as .animate() calls it for us
          }, self.options.delay);
      
          return self;
      };

      开始定时器。

      3.2.10 stop方法

      self.stop = function() {
          clearTimeout(self.interval);
      
          return self;
      };

      清除定时器。

      3.2.11 next方法

      self.next = function() {
          //下一个
          var target = self.current + 1;
      
          // 如果大于总数,就回到开始
          if(target >= self.total) {
              target = 0;
          }
          //交给self.animate方法去完成
          return self.animate(target, 'next');
      };

      3.2.12 prev方法

      self.prev = function() {
          return self.animate(self.current - 1, 'prev');
      };

      和self.next()方法类是,self.animate方法能够支持`animate(-1, 'prev')的写法,不需要出入target参数。

      3.2.13 animate方法

      虽然方法名叫animate但是其实并没有真正动起来,最终还是交给三种不同轮播效果的animate开头的函数,如animateHorizontalanimateVerticalanimateFade

      //  Despite the name, this doesn't do any animation - since there's
      //  now three different types of animation, we let this method delegate
      //  to the right type, keeping the name for backwards compat.
      self.animate = function(to, dir) {
          //  Animation shortcuts
          //  Instead of passing a number index, we can now
          //  use .data('unslider').animate('last');
          //  or .unslider('animate:last')
          //  to go to the very last slide
          if(to === 'first') to = 0;
          if(to === 'last') to = self.total;
      
          //  Don't animate if it's not a valid index
          if(isNaN(to)) {
              return self;
          }
      
          if(self.options.autoplay) {
              self.stop().start();
          }
          //设置了目标序号
          self.setIndex(to);
      
          //触发unslider.change事件
          //个人觉得自定义的事件最好不要用.号分隔
          self.$context.trigger(self._ + '.change', [to, self.$slides.eq(to)]);
      
          //  Delegate the right method - everything's named consistently
          //  so we can assume it'll be called "animate" + 
          var fn = 'animate' + $._ucfirst(self.options.animation);
      
          //  Make sure it's a valid animation method, otherwise we'll get
          //  a load of bug reports that'll be really hard to report
          if($.isFunction(self[fn])) {
              //self.current已经在setIndex方法中修改了
              self[fn](self.current, dir); 
          }
      
          return self;
      };

      3.2.14 setIndex方法

      这个方法会修改self.current属性。

      self.setIndex = function(to) {
          //处理负数的情况
          if(to < 0) {
              to = self.total - 1;
          }
          //current不能超过self.total -1
          self.current = Math.min(Math.max(0, to), self.total - 1);
          //如果支持导航,需要将相应的导航元素设置active类
          if(self.options.nav) {
              self.$nav.find('[data-slide="' + self.current + '"]')._active(self.options.activeClass);
          }
          //设置选中的轮播页面的active类
          self.$slides.eq(self.current)._active(self.options.activeClass);
      
          return self;
      };

      self.$navself.$slides都有调用$.fn._active,这个类能够做到的是,将自己jQuery对象增加active类,并将所有兄弟元素对象移除active类。

      3.3 轮播动画

      这一版的unslider支持三种类型的动画,左右、垂直方向轮播、还有就是fade(翻译成闪现合理么?),分别对应animateHorizotalanimateVerticalanimateFade三种方法。

      3.3.1 animateHorizontal方法

      self.animateHorizontal = function(to) {
          var prop = 'left';
      
          //  Add RTL support, slide the slider
          //  the other way if the site is right-to-left
          if(self.$context.attr('dir') === 'rtl') {
              prop = 'right';
          }
          //见前面self.initInfinite解释
          //如果self.options.infinite是true,在开始和结束位置都会多增加克隆的页面元素
          //所以这里需要减去相应的宽度
          if(self.options.infinite) {
              //  So then we need to hide the first slide
              self.$container.css('margin-' + prop, '-100%');
          }
          //委托给slide方法,to是序号
          return self.slide(prop, to);
      };

      animateHorizontal是由animate调用的,原来的参数中animate(to, dir)to被修正为目标序号并设置到self.current变量后,调用animateHorizontal方法传入animateHorizontal(self.current,dir),到了这里似乎dir参数被丢弃了(不明白)。

      3.3.2 animateVertical方法

      self.animateVertical = function(to) {
          self.options.animateHeight = true;
      
          //  Normal infinite CSS fix doesn't work for
          //  vertical animation so we need to manually set it
          //  with pixels. Ah well.
          //减去自身的高度
          if(self.options.infinite) {
              self.$container.css('margin-top', -self.$slides.outerHeight());
          }
      
          return self.slide('top', to);
      };

      3.3.3 animateFade方法

      3.3.4 slide方法

      真正轮播页面的方法。

      self.slide = function(prop, to) {
          //  If we want to change the height of the slider
          //  to match the current slide, you can set
          //  {animateHeight: true}
          if(self.options.animateHeight) {
              self._move(self.$context, {height: self.$slides.eq(to).outerHeight()}, false);
          }
      
          //  For infinite sliding we add a dummy slide at the end and start
          //  of each slider to give the appearance of being infinite
          // 处理参数infinite是true的情况
          if(self.options.infinite) {
              var dummy;
      
              //  Going backwards to last slide
              if(to === self.total - 1) {
                  //  We're setting a dummy position and an actual one
                  //  the dummy is what the index looks like
                  //  (and what we'll silently update to afterwards),
                  //  and the actual is what makes it not go backwards
                  dummy = self.total - 3;
                  to = -1;
              }
      
              //  Going forwards to first slide
              if(to === self.total - 2) {
                  dummy = 0;
                  to = self.total - 2;
              }
      
              //  If it's a number we can safely set it
              if(typeof dummy === 'number') {
                  self.setIndex(dummy);
      
                  //  Listen for when the slide's finished transitioning so
                  //  we can silently move it into the right place and clear
                  //  this whole mess up.
                  self.$context.on(self._ + '.moved', function() {
                      if(self.current === dummy) {
                          self.$container.css(prop, -(100 * dummy) + '%').off(self._ + '.moved');
                      }
                  });
              }
          }
      
          //  We need to create an object to store our property in
          //  since we don't know what it'll be.
          var obj = {};
      
          //  Manually create it here
          obj[prop] = -(100 * to) + '%';
      
          //  And animate using our newly-created object
          return self._move(self.$container, obj);
      };

      处理self.options.infinite参数为true的情况时,源码中有些指定的数字,不知道是何依据。
      内部定义的obj,最后传给self._move方法,轮播功能进一步委托给self._move来完成。做下假定,如果我们使用默认参数,即水平轮播,并假设需要轮播到第二个页面,此时后面的代码最终效果如下:

      var obj = {};
      obj["left"] = -(100 * 1) + '%';
      return self._move(self.$container, obj);

      3.4 事件绑定

      unslider自定义了几种事件,包括unslider.change、unslider.ready和unslider.moved等,而在绑定导航元素的点击事件时使用了命名空间的形式。命名空间由self.eventSuffix指定。

      3.4.1 unslider自定义事件

      请参考API文档。

      3.4.3 命名空间的click事件

      参考self.initNav部分说明。

      3.5 其他方法

      3.5.1 $.Unslider._move方法

      self._move = function($el, obj, callback, speed) {
          //回调处理
          if(callback !== false) {
              callback = function() {
                  self.$context.trigger(self._ + '.moved');
              };
          }
          //调用$.fn._move方法
          return $el._move(obj, speed || self.options.speed, self.options.easing, callback);
      };

      $el是有animateHorizontal方法调用self._move传入的self.$container,即对应的ul层。

      3.5.2 $.fn._move方法

      $.fn._move = function() {
          //停止所有动画,参照jQuery的animate说明文档
          this.stop(true, true);
          //如果没有添加velocity支持,最终动画还是由$.fn.animate方法来完成
          return $.fn[$.fn.velocity ? 'velocity' : 'animate'].apply(this, arguments);
      };

      根据前面的说明,最终交由$.fn.animate方法来挖成动画。按照之前的假设,此时这里的效果如下面代码所示:

      var obj = {"left": "-100%"};
      return $.fn.animate.apply(self.$container, arguments);
      //arguments有obj, speed, callback等参数

      至此,整个轮播过程的调用过程就分析完毕。