5.11.4 源码分析
在写这一小节的时候,我有一点小担心,我个人认为,旋转轮播的JavaScript代码是所有Bootstrap插件里最复杂的。所以在分析的时候,将配合一些HTML和实时的CSS以及图片来讲解。
步骤1 立即调用的函数。此步骤与Modal插件的步骤1一样,此处不赘述。
步骤2 插件核心代码。旋转轮播的核心代码比较复杂,在理解了CSS原理之后,先看一下概要代码。
- var Carousel = function (element, options) {
- this.$element = $(element) // 容器元素,因为不管单击哪个,最终都会转换到
- // data-ride= "carousel"容器元素
- this.$indicators = this.$element.find('.carousel-indicators')
- // 查找小圆圈指示符元素集合
- this.options = options
- // 插件运行参数,优先级最高的是所单击元素上的data-属性,然后是容器上的data-属性,最后才是默认值
- this.paused = // 暂停标记
- this.sliding = // 轮播标记
- this.interval = // 轮播间隔标记
- this.$active = // 当前活动图片的对象
- this.$items = null // 所有的图片元素对象
- this.options.pause == 'hover' && this.$element // 如果设置鼠标移动上去就暂停的话
- .on('mouseenter', $.proxy(this.pause, this)) // 鼠标进入时,执行pause方法进行暂停
- .on('mouseleave', $.proxy(this.cycle, this)) // 鼠标移出时,执行cycle方法重启开启
- }
- Carousel.DEFAULTS = {
- interval: 5000, // 默认间隔5秒
- pause: 'hover', // 默认设置,鼠标移动上去图片就暂停
- wrap: true // 轮播是否持续循环
- }
- // 开启轮播(默认从右向左)
- Carousel.prototype.cycle = function (e) {};
- // 判断当前图片在整个轮播图片集的索引
- Carousel.prototype.getActiveIndex = function (){};
- // 直接轮播指定索引的图片
- Carousel.prototype.to = function (pos) {};
- // 暂停轮播
- Carousel.prototype.pause = function (e) {};
- // 轮播下一张图片
- Carousel.prototype.next = function (){};
- // 轮播上一张图片
- Carousel.prototype.prev = function (){};
- // 轮播的具体操作方法
- Carousel.prototype.slide = function (type, next) {};
上述7个原型方法,前6个都是为第7个slide方法做辅助工作的,比如处理上一张、下一张、找到当前图片在图片集中的索引、判断移动方向、直接定位到指定的图片上等类似工作,而slide是真正执行轮播动画过渡效果的方法。先看前6个方法的详细代码:
- // 开启轮播(默认从右向左)
- Carousel.prototype.cycle = function (e) {
- e || (this.paused = false) // 如果没传e,将paused设置为false
- this.interval && clearInterval(this.interval) // 如果设置了interval间隔,就清除它
- // 如果设置了options.interval间隔,并且没有暂停
- // 就将在下一个间隔之后,执行next方法(播放下一张图片)
- this.options.interval
- && !this.paused
- && (this.interval = setInterval($.proxy(this.next, this), this.options.interval))
- return this // 返回this,以便链式操作
- }
- // 判断当前图片在整个轮播图片集中的索引
- Carousel.prototype.getActiveIndex = function () {
- this.$active = this.$element.find('.item.active') // 找到当前active图片元素(其实是元素
- // 外部的div容器)
- // 在找到该元素的父容器(即carousel-inner样式容器)的子集合(即所有的item元素集合)
- this.$items = this.$active.parent().children()
- return this.$items.index(this.$active) // 判断当前图片元素在集合中的索引位置,并返回
- }
- // 直接轮播指定索引的图片
- Carousel.prototype.to = function (pos) {
- var that = this
- var activeIndex = this.getActiveIndex() // 查找当前图片的索引位置
- // 如果传入的pos值大于图片总数,或者小于0,则直接返回不做任何操作
- if (pos > (this.$items.length - 1) || pos < 0) return
- // 如果正在执行其他图片轮播,则在其结束以后再跳转到指定的pos图片(通过触发一次性的slid事件来实现)
- if (this.sliding) return this.$element.one('slid.bs.carousel', function () { that.to(pos) })
- // 如果当前活动图片正好是指定的pos图片,则先暂停,然后继续执行
- if (activeIndex == pos) return this.pause().cycle()
- // 如果pos大于当前活动图片的索引,则传入next方法,否则是prev方向
- // 第二个参数是将pos对应的item元素对象传进去(具体作用查看下面的slide方法)
- return this.slide(pos > activeIndex ? 'next' : 'prev', $(this.$items[pos]))
- }
- // 暂停轮播
- Carousel.prototype.pause = function (e) {
- e || (this.paused = true) // 如果没传e,将paused设置为true(说明要暂停)
- // 如果有next或prev元素,并且支持动画,则触发动画
- if (this.$element.find('.next, .prev').length && $.support.transition) {
- this.$element.trigger($.support.transition.end) // 触发动画
- this.cycle(true) // 开始执行(注意传入了true参数)
- }
- this.interval = clearInterval(this.interval)
- return this // 返回this,以便链式操作
- }
- // 轮播下一张图片
- Carousel.prototype.next = function () {
- if (this.sliding) return // 如果正在轮播(还没结束),直接返回
- return this.slide('next') // 否则,轮播下一张图片
- }
- // 轮播上一张图片
- Carousel.prototype.prev = function () {
- if (this.sliding) return // 如果正在轮播(还没结束),直接返回
- return this.slide('prev') // 否则,轮播上一张图片
- }
上述6个原型方法里,主要是利用了jQuery的基础操作方法,来对各个图片元素进行查找,并设置各种状态,但最终都会调用slide方法。在分析slide方法之前,我们先来理一下思路。
首先假定一个场景,有3张图片需要轮播,方向是从右向左,并且假定当前第2张图片B正在处理显示状态,效果如图5-19所示。
图5-19 图B在高亮状态
这时候,C要向左移动,也就意味着A和B都要向左移动(或者是隐藏掉),这时候C才能达到中间的显示区域,效果如图5-20所示。
图5-20 图B左移,图C移动到高亮状态
C一旦达到中间的显示区域以后,由于只有3张图片,所以C的下一张就应该是第二轮的A图片。当然,默认参数里有个wrap来设置是否循环滚动,如果是false,那C显示以后,就停止不动了。剩余几个原型方法的详细代码如下:
- // 轮播的具体操作方法
- Carousel.prototype.slide = function (type, next) {
- var $active = this.$element.find('.item.active') // 找到当前活动的图片对象条目
- // 如果提供了next参数,就使用这个参数,如果没提供,就使用当前活动条目的下一个图片条目
- var $next = next || $active[type]()
- var isCycling = this.interval
- // 获取移动的方向:如果是next,则是向左移动,否则是向右移动
- var direction = type == 'next' ? 'left' : 'right'
- // 如果获取失败,指定一个元素进行特殊处理,如果再传next,则指向下一轮的图片
- // 即如果最后一个图片显示以后,还要next,那就是下一轮的first
- var fallback = type == 'next' ? 'first' : 'last'
- var that = this // 获取当前调用者的this对象,防止作用域污染
- if (!$next.length) { // 如果下一个对象不存在
- if (!this.options.wrap) return // 判断wrap是否为假,如果是,则直接返回
- $next = this.$element.find('.item')[fallback]()
- // 否则,使用fallback指定的元素当做 $next对象元素
- }
- // 如果下一个元素已经是高亮了,则设置轮播标记为false
- if ($next.hasClass('active')) return this.sliding = false
- // 设定轮播后要触发的事件,以及要暴露的参数
- var e = $.Event('slide.bs.carousel', { relatedTarget: $next[0], direction: direction })
- this.$element.trigger(e) // 触发slide事件
- if (e.isDefaultPrevented()) return // 如果要轮播的对象已经是active高亮了,直接返回不做处理
- this.sliding = true // 标记轮播正在进行
- isCycling && this.pause() // 如果有间隔,则暂停自动执行
- // 处理小圆圈的高亮状态
- if (this.$indicators.length) { // 如果有小圆圈指示符
- this.$indicators.find('.active').removeClass('active')
- // 去除原来高亮指示符的active样式
- // 设置一次性slid事件,以便在轮播后执行该事件,从而设置高亮指示符
- this.$element.one('slid.bs.carousel', function () {
- var $nextIndicator = $(that.$indicators.children()[that.getActiveIndex()])
- // 获取当前高亮图片的索引,按照该索引找到对的指示符
- $nextIndicator && $nextIndicator.addClass('active')
- // 如果找到的话,就添加active样式使其高亮
- })
- }
- // 如果支持动画,并且设置了slide样式(注意,这里不是fade效果)
- if ($.support.transition && this.$element.hasClass('slide')) {
- $next.addClass(type) // 给要轮播的元素添加type类型样式(比如:next、prev)
- $next[0].offsetWidth // 重绘UI
- $active.addClass(direction) // 给当前活动的对象添加方法(如left、right)
- $next.addClass(direction) // 给要轮播的元素添加方法(如left、right)
- // 给当前活动元素绑定一次性动画事件,在该事件回调里执行以下操作
- $active
- .one($.support.transition.end, function () {
- // 在将要轮播的元素上,删除对应type和方向样式(如next left或者prev right),
- // 然后添加active样式
- $next.removeClass([type, direction].join(' ')).addClass('active')
- // 删除当前活动元素(即将隐藏)上的active样式和方向样式(如left或right)
- $active.removeClass(['active', direction].join(' '))
- that.sliding = false // 设置轮播状态结束
- // 然后触发slid事件,这里使用了setTimeout是确保UI刷新线程不被阻塞
- setTimeout(function () { that.$element.trigger('slid.bs.carousel') }, 0)
- })
- .emulateTransitionEnd($active.css('transition-duration').slice(0, -1) * 1000)
- } else { // 如果不支持动画
- $active.removeClass('active') // 删除当前高亮元素上的active样式
- $next.addClass('active') // 给要轮播的元素上添加高亮active样式
- this.sliding = false // 设置轮播状态结束
- this.$element.trigger('slid.bs.carousel') // 触发slid事件
- }
- isCycling && this.cycle() // 如果有间隔,则重新开始(间隔后)自动执行
- return this // 返回this,以便链式操作(这里的this是data-ride="carousel"容器元素)
- }
根据上述步骤可以得知,前面几行代码都是处理方向的,然后在执行的时候先判断是否支持动画(以及有否slide样式),如果不支持就直接把原来图片元素上的active删除,在新显示图片上加上active样式即可;而如果支持动画,则明显很复杂。我们来用下面的HTML运行结果来分析,首先B处于active状态,HTML代码如下:
- <div class="item"><img alt="A图" src="A" /></div>
- <div class="item active"><img alt="B图" src="B" /></div>
- <div class="item"><img alt="C图" src="C" /></div>
在触发slide.bs.carousel事件后,如果不阻止默认工作,就执行如下操作:
- $next.addClass(type) // 给要轮播的元素添加type类型样式(比如:next、prev)
- $next[0].offsetWidth // 重绘UI
- $active.addClass(direction) // 给当前活动的对象添加方法(如left、right)
- $next.addClass(direction) // 给要轮播的元素添加方法(如left、right)
也就是,首先给元素C添加一个next样式,此时是第一次变化。
- <div class="item"><img alt="A图" src="A" /></div>
- <div class="item active"><img alt="B图" src="B" /></div>
- <div class="item next"><img alt="C图" src="C" /></div>
然后通过调用$next[0].offsetWidth,重绘UI,以便能够生效,再给元素C添加left样式,并且也给原来的图片B也添加一个left样式,此时HTML结构变成如下这样(此时是第二次变化):
- <div class="item"><img alt="A图" src="A" /></div>
- <div class="item active left"><img alt="B图" src="B" /></div>
- <div class="item next left"><img alt="C图" src="C" /></div>
此时,我们再来看一下CSS里所定义的相关样式(我们这里只看next和left相关的,如果是反方向的prev和right相关的,请自行查看CSS源码)。
- // 源码5450行
- .carousel-inner > .next,
- .carousel-inner > .prev { /*如果是即将轮播的元素*/
- position: absolute; /*绝对定位*/
- top: 0; /*顶部间距为0*/
- width: 100%;
- }
- .carousel-inner > .next { /*如果是下一张图片要显示*/
- left: 100%; /*则left为100%,表示在当前显示图片的右边,利用上面的overflow:
- hidden;先隐藏起来*/
- }
- .carousel-inner > .prev { /*如果上一张图片即将显示了*/
- left: -100%; /*则left为-100%,表示在当前显示图片的左边,利用上面的overflow:
- hidden;先隐藏起来*/
- }
- .carousel-inner > .next.left,
- .carousel-inner > .prev.right { /*如果下一张图片即将显示了*/
- left: 0; /*则设置左对齐,然后利用动画滚动到左边*/
- }
- .carousel-inner > .active.left { /*如果滚动方向是向左*/
- left: -100%; /*则上一张高亮显示的照片向左移动100%的距离,以便隐藏起来*/
- }
- .carousel-inner > .active.right { /*如果滚动方向是向右*/
- left: 100%; /*则上一张高亮显示的照片向右移动100%的距离,以便隐藏起来*/
- }
在重绘UI线程之前,元素C的left值是100%;再次添加left样式后,left值变成了0,而原来的元素B的left值则变成了-100%,表示已经移出显示区域了。
与此同时,定义一个一次性动画事件,并在600毫秒以后执行。
- $active.one($.support.transition.end, function () {
- // 给当前活动元素绑定一次性动画事件,在该事件回调里执行如下操作
- $next.removeClass([type, direction].join(' ')).addClass('active')
- // 在将要轮播元素上,删除对应type和方向的样式(如next left或者prev right),然后添加active样式
- $active.removeClass(['active', direction].join(' '))
- // 删除当前活动元素(即将隐藏)上的active样式和方向样式(如left或right)
- that.sliding = false // 设置轮播状态结束
- setTimeout(function () { that.$element.trigger('slid') }, 0)
- // 然后触发slid事件,这里使用了setTimeout是确保UI刷新线程不被阻塞
- }).emulateTransitionEnd(600)
在回调函数里,我们可以看出,首先删除了元素C上的next和left样式,添加了active样式;然后删除了元素B上的active和left样式;最后设置轮播状态结束,触发slid事件。其结果HTML结构如下(最终状态):
- <div class="item"><img alt="A图" src="A" /></div>
- <div class="item "><img alt="B图" src="B" /></div>
- <div class="item active "><img alt="C图" src="C" /></div>
和初始HTML代码相比,其实就是把active样式从B的div上,拖到了C的div上,就这么简单。当然,有的人可能还会问到为什么最后一步触发slid事件用了setTimeout函数。
- setTimeout(function () { that.$element.trigger('slid') }, 0)
- // 然后触发slid事件,这里使用了setTimeout是确保UI刷新线程不被阻塞
这主要是因为,在默认情况下,浏览器上执行的JavaScript代码和UI更新是属于同一个现场,所以如果直接触发slid的话,可能会导致阻塞UI更新。UI线程的阻塞很多时候是由于我们要在代码里进行长时间的脚本运算,超过了浏览器限制,导致浏览器失去响应,造成假死的状态。使用setTimeout则表示,暂时放开线程的控制器,以便让UI能够立即开始更新,然后再执行setTimeout内部的代码,从而达到平滑的效果。
Slide方法分析完了,理解以后,大家可能就觉得真不错(或者是:也就这么回事嘛)。是的,正是这么简单的代码才构成了这么炫的特效。
步骤3 jQuery插件定义。jQuery插件的定义和其他插件类似。这里我们需要注意一下,当option传入为数字(number)的时候,是表示直接显示特定的图片,也就是调用该实例的.to(number)方法。源码如下:
- var old = $.fn.carousel
- // 保留其他库的$.fn.carousel代码(如果定义的话),以便在noConflict之后,可以继续使用该老代码
- $.fn.carousel = function (option) {
- return this.each(function () { // 遍历所有符合规则的元素
- var $this = $(this) // 当前触发元素的jQuery对象
- var data = $this.data('bs.carousel')
- // 获取自定义属性data-bs.carousel的值 (其实是
- // carousel实例)
- // 合并参数,优先级依次递增
- // var options = $.extend({}, Carousel.DEFAULTS, $this.data(), typeof option ==
- // 'object' && option)
- // 如果option参数是字符串,直接使用,否则使用options里的slide参数
- var action = typeof option == 'string' ? option : options.slide
- // 如果没有carousel实例,就初始化一个,并传入this和参数
- if (!data) $this.data('bs.carousel', (data = new Carousel(this, options)))
- // 如果option是数字,表示是想直接切换到某张图上,所以直接使用.to()方法
- if (typeof option == 'number') data.to(option)
- else if (action) data[action]() // 否则,再判断如果action存在,就执行action
- // 所对应的方法
- else if (options.interval) data.pause().cycle()
- // 最后,如果指定了interval参数,先暂停然后重新循环
- })
- }
- $.fn.carousel.Constructor = Carousel // 并重设插件构造器,可以通过该属性获取插件的真实类函数
步骤4 防冲突处理。此步骤与Modal插件的步骤4一样,此处不赘述。
步骤5 绑定触发事件。该步骤的触发代码比较复杂,主要是和其他的插件有所不同,该插件在触发元素上绑定事件,但并没有将触发元素作为Carousel类的实例。具体如下:
- // 绑定触发事件
- // 在带有data-slide或data-slide-to属性的元素上绑定事件
- $(document).on('click.bs.carousel.data-api', '[data-slide], [data-slide-to]', function (e) {
- var $this = $(this), href
- // 查找target,即所指定的折叠地区的id或者选择符,如果没有target,就使用href里的值
- var $target = $($this.attr('data-target') || (href = $this.attr('href')) &&
- href.replace(/.*(?=#[^\s]+$)/, '')) // strip for ie7
- // 合并target上的data-属性和触发元素上的data-属性
- var options = $.extend({}, $target.data(), $this.data())
- // 查找单击元素上是否有data-slide-to属性
- // 如果存在,则取消间隔设置(因为单击data-slide-to意味着是手动触发行为,后续是不会循环播放的)
- var slideIndex = $this.attr('data-slide-to')
- if (slideIndex) options.interval = false
- $target.carousel(options) // 实例化插件
- // 再次判断如果单击的是小圆圈data-slide-to,
- // 则直接跳转到那张图上($target.data('bs.carousel')是绑定的插件实例)
- if (slideIndex = $this.attr('data-slide-to')) {
- $target.data('bs.carousel').to(slideIndex)
- }
- e.preventDefault() // 阻止默认行为
- })
- $(window).on('load', function () {
- $('[data-ride="carousel"]').each(function () { // 遍历所有符合规则的元素
- var $carousel = $(this)
- $carousel.carousel($carousel.data())// 实例化插件(并收集data-参数),以便自动运行
- })
- })
通过上述代码可以看到,该触发代码有两部分。第一部分是对左右控制链接和圆圈指示符(也就是带data-slide、data-slide-to属性的元素)绑定click事件;并且判断如果单击了data-slide-to,则直接显示特定的图片。这里有一点需要注意的是:一旦选择了特定图片,options.interval参数就会被设置为false,也就是不自动轮播,但这取决于初始的轮播是否设置了interval参数,如果设置的话,则还是会自动轮播;如果没有设置,在显示指定的图片之后就会停止。第二部分是通用的插件初始化代码,就不多说了。