unslider源码分析

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

1. 如何使用

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

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

<div class="banner">
    <ul>
        <li>This is a slide.</li>
        <li>This is another slide.</li>
        <li>This is a final slide.</li>
    </ul>
</div>

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

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

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

源代码

<div class="banner">
  <ul>
    <li style="background-image: url('img/sunset.jpg');">
      <div class="inner">
        <h1>The jQuery slider that just slides.</h1>
        <p>就是这个不到3kb的插件!没有奇特的特效或无用的标签。</p>

        <a class="btn" href="#download">下载</a>
      </div>
    </li>

    <li style="background-image: url('img/wood.jpg');">
      <div class="inner">
        <h1>Fluid, flexible, fantastically minimal.</h1>
        <p>Use any HTML in your slides, extend with CSS. You have full control.</p>

        <a class="btn" href="#download">下载</a>
      </div>
    </li>

    <li style="background-image: url('img/subway.jpg');">
      <div class="inner">
        <h1>开源</h1>
        <p>Unslider的所有源码都托管在GitHub上。</p>

        <a class="btn" href="//github.com/idiot/unslider">参与</a>
      </div>
    </li>

    <li style="background-image: url('img/shop.jpg');">
      <div class="inner">
        <h1>Uh, that’s about it.</h1>
        <p>I just wanted to show you another slide.</p>

        <a class="btn" href="#download">下载</a>
      </div>
    </li>
  </ul>
</div>

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

<div class="unslider">
    <div class="banner unslider-horizontal" style="overflow: hidden;">
    <ul class="unslider-wrap unslider-carousel" style="width: 400%; left: 0%;">
        <li style="width: 25%; class="unslider-active">
        </li>

        <li style="width: 25%; class="">
        </li>

        <li style="width: 25%; class="">
        </li>

        <li style="width: 25%; class="">
        </li>
    </ul>
    </div>
    <a class="unslider-arrow next">Next</a>
    <a class="unslider-arrow prev">Prev</a>
    <nav class="unslider-nav">
        <ol>
        <li data-slide="0" class="unslider-active">1</li>
        <li data-slide="1" class="">2</li>
        <li data-slide="2" class="">3</li>
        <li data-slide="3" class="">4</li>
        </ol>
    </nav>
</div>

可以发现使用插件后,会在.banner上封装<div class="unslider"></div>,并且对.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 => <div class="unslider"></div>
     *  self.$context => <div class="banner"></div>
     *  self.$container => <ul class="unslider-wrap"></ul>
     *  self.slides =>     <li></li>
     */
    //备份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: '<a class="' + self._ + '-arrow prev">Prev</a>',
            next: '<a class="' + self._ + '-arrow next">Next</a>'
        },

        // 方向
        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('<div class="' + self._ + '" />');
    //备份容器的父层,即刚才的封装层
    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 = $('<nav class="' + self.prefix + 'nav"><ol /></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('<li data-slide="' + key + '">' + label + '</li>');
    });
    
    //  插入到$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等参数

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

3.5.3 $.fn._active方法

3.5.4 $._ucfirst方法

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏搞前端的李蚊子

vue.js使用props在父子组件之间传参

本篇文章是我参考官方文档整理的,供大家参考,高手勿喷! prop 组件实例的作用域是孤立的。这意味着不能 (也不应该) 在子组件的模板内直接引用父组件的数据。要...

33740
来自专栏HTML5学堂

JS简单页面交互实战 - 点击按钮实现求和功能

上一期堡堡给大家讲解了简单的页面交互效果 - 点击块,让块动起来,让我们更清晰的了解JS逻辑和DOM的结合。如果想具体了解点击块,让块动起来,可以回复“交互”到...

54780
来自专栏Golang语言社区

Go-指针、传值与传引用、垃圾回收

要点 Go使用的*、&、new()这些运算符,和C++的用法完全一样。 有传值和传引用/传地址的概念,和C++一样。 Go没有new对应的delete操作,而是...

376100
来自专栏Golang语言社区

go 切片使用小结

最新项目使用go语言开发,因此有机会结识了go语言。在写代码时,无意间发现了同事代码的一个bug。今天拿来一起学习一下。 首先go语言有个强大的基本数据结构,那...

34780
来自专栏前端知识分享

第22天:js改变样式效果

1、alert:弹出警示框(用的非常少,用户体验不好) 完整写法:window.alert(“执行语句”); window对象,窗口,一般情况可省略 alert...

14410
来自专栏小壮和前端

深入理解es6的promise

我的博客即将搬运同步至腾讯云+社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?inv...

7610
来自专栏Golang语言社区

golang(Go语言) byte/[]byte 与 二进制形式字符串 互转

效果 把某个字节或字节数组转换成字符串01的形式,一个字节用8个”0”或”1”字符表示。比如: byte(3) –> “00000011” []byte{1...

53170
来自专栏十月梦想

Vue入门详解之模板语法插值表达式,v-html和v-text

我们来看一下简单的模板语法,在Vue中有三种模板语法用来渲染model(数据层),插值表达式(就是{{}},双花括号表达法),v-text和v-html三种!那...

58330
来自专栏WindCoder

mybatis在xml文件中处理大于号小于号的方法(mybatis大于小于的转义)

SELECT * FROM test WHERE 1 = 1 AND start_date  &lt;= CURRENT_DATE AND end_date &...

14910
来自专栏冷冷

Jquery 获取checkbox属性checked为undefined和改变状态 值不变

API: As of jQuery 1.6, the .attr() method returns undefined for attributes that ...

24260

扫码关注云+社区

领取腾讯云代金券