5.11.4 源码分析

在写这一小节的时候,我有一点小担心,我个人认为,旋转轮播的JavaScript代码是所有Bootstrap插件里最复杂的。所以在分析的时候,将配合一些HTML和实时的CSS以及图片来讲解。

步骤1 立即调用的函数。此步骤与Modal插件的步骤1一样,此处不赘述。

步骤2 插件核心代码。旋转轮播的核心代码比较复杂,在理解了CSS原理之后,先看一下概要代码。

  1. var Carousel = function (element, options) {
  2. this.$element = $(element) // 容器元素,因为不管单击哪个,最终都会转换到
  3. // data-ride= "carousel"容器元素
  4. this.$indicators = this.$element.find('.carousel-indicators')
  5. // 查找小圆圈指示符元素集合
  6. this.options = options
  7. // 插件运行参数,优先级最高的是所单击元素上的data-属性,然后是容器上的data-属性,最后才是默认值
  8. this.paused = // 暂停标记
  9. this.sliding = // 轮播标记
  10. this.interval = // 轮播间隔标记
  11. this.$active = // 当前活动图片的对象
  12. this.$items = null // 所有的图片元素对象
  13.  
  14. this.options.pause == 'hover' && this.$element // 如果设置鼠标移动上去就暂停的话
  15. .on('mouseenter', $.proxy(this.pause, this)) // 鼠标进入时,执行pause方法进行暂停
  16. .on('mouseleave', $.proxy(this.cycle, this)) // 鼠标移出时,执行cycle方法重启开启
  17. }
  18.  
  19. Carousel.DEFAULTS = {
  20. interval: 5000, // 默认间隔5秒
  21. pause: 'hover', // 默认设置,鼠标移动上去图片就暂停
  22. wrap: true // 轮播是否持续循环
  23. }
  24.  
  25. // 开启轮播(默认从右向左)
  26. Carousel.prototype.cycle = function (e) {};
  27. // 判断当前图片在整个轮播图片集的索引
  28. Carousel.prototype.getActiveIndex = function (){};
  29. // 直接轮播指定索引的图片
  30. Carousel.prototype.to = function (pos) {};
  31. // 暂停轮播
  32. Carousel.prototype.pause = function (e) {};
  33. // 轮播下一张图片
  34. Carousel.prototype.next = function (){};
  35. // 轮播上一张图片
  36. Carousel.prototype.prev = function (){};
  37. // 轮播的具体操作方法
  38. Carousel.prototype.slide = function (type, next) {};

上述7个原型方法,前6个都是为第7个slide方法做辅助工作的,比如处理上一张、下一张、找到当前图片在图片集中的索引、判断移动方向、直接定位到指定的图片上等类似工作,而slide是真正执行轮播动画过渡效果的方法。先看前6个方法的详细代码:

  1. // 开启轮播(默认从右向左)
  2. Carousel.prototype.cycle = function (e) {
  3. e || (this.paused = false) // 如果没传e,将paused设置为false
  4.  
  5. this.interval && clearInterval(this.interval) // 如果设置了interval间隔,就清除它
  6.  
  7. // 如果设置了options.interval间隔,并且没有暂停
  8. // 就将在下一个间隔之后,执行next方法(播放下一张图片)
  9. this.options.interval
  10. && !this.paused
  11. && (this.interval = setInterval($.proxy(this.next, this), this.options.interval))
  12.  
  13. return this // 返回this,以便链式操作
  14. }
  15. // 判断当前图片在整个轮播图片集中的索引
  16. Carousel.prototype.getActiveIndex = function () {
  17. this.$active = this.$element.find('.item.active') // 找到当前active图片元素(其实是元素
  18. // 外部的div容器)
  19.  
  20. // 在找到该元素的父容器(即carousel-inner样式容器)的子集合(即所有的item元素集合)
  21. this.$items = this.$active.parent().children()
  22.  
  23. return this.$items.index(this.$active) // 判断当前图片元素在集合中的索引位置,并返回
  24. }
  25. // 直接轮播指定索引的图片
  26. Carousel.prototype.to = function (pos) {
  27. var that = this
  28. var activeIndex = this.getActiveIndex() // 查找当前图片的索引位置
  29.  
  30. // 如果传入的pos值大于图片总数,或者小于0,则直接返回不做任何操作
  31. if (pos > (this.$items.length - 1) || pos < 0) return
  32. // 如果正在执行其他图片轮播,则在其结束以后再跳转到指定的pos图片(通过触发一次性的slid事件来实现)
  33. if (this.sliding) return this.$element.one('slid.bs.carousel', function () { that.to(pos) })
  34. // 如果当前活动图片正好是指定的pos图片,则先暂停,然后继续执行
  35. if (activeIndex == pos) return this.pause().cycle()
  36.  
  37. // 如果pos大于当前活动图片的索引,则传入next方法,否则是prev方向
  38. // 第二个参数是将pos对应的item元素对象传进去(具体作用查看下面的slide方法)
  39. return this.slide(pos > activeIndex ? 'next' : 'prev', $(this.$items[pos]))
  40. }
  41. // 暂停轮播
  42. Carousel.prototype.pause = function (e) {
  43. e || (this.paused = true) // 如果没传e,将paused设置为true(说明要暂停)
  44.  
  45. // 如果有next或prev元素,并且支持动画,则触发动画
  46. if (this.$element.find('.next, .prev').length && $.support.transition) {
  47. this.$element.trigger($.support.transition.end) // 触发动画
  48. this.cycle(true) // 开始执行(注意传入了true参数)
  49. }
  50. this.interval = clearInterval(this.interval)
  51. return this // 返回this,以便链式操作
  52. }
  53. // 轮播下一张图片
  54. Carousel.prototype.next = function () {
  55. if (this.sliding) return // 如果正在轮播(还没结束),直接返回
  56. return this.slide('next') // 否则,轮播下一张图片
  57. }
  58. // 轮播上一张图片
  59. Carousel.prototype.prev = function () {
  60. if (this.sliding) return // 如果正在轮播(还没结束),直接返回
  61. return this.slide('prev') // 否则,轮播上一张图片
  62. }

上述6个原型方法里,主要是利用了jQuery的基础操作方法,来对各个图片元素进行查找,并设置各种状态,但最终都会调用slide方法。在分析slide方法之前,我们先来理一下思路。

首先假定一个场景,有3张图片需要轮播,方向是从右向左,并且假定当前第2张图片B正在处理显示状态,效果如图5-19所示。

5.11.4 源码分析 - 图1 图5-19 图B在高亮状态

这时候,C要向左移动,也就意味着A和B都要向左移动(或者是隐藏掉),这时候C才能达到中间的显示区域,效果如图5-20所示。

5.11.4 源码分析 - 图2 图5-20 图B左移,图C移动到高亮状态

C一旦达到中间的显示区域以后,由于只有3张图片,所以C的下一张就应该是第二轮的A图片。当然,默认参数里有个wrap来设置是否循环滚动,如果是false,那C显示以后,就停止不动了。剩余几个原型方法的详细代码如下:

  1. // 轮播的具体操作方法
  2. Carousel.prototype.slide = function (type, next) {
  3. var $active = this.$element.find('.item.active') // 找到当前活动的图片对象条目
  4.  
  5. // 如果提供了next参数,就使用这个参数,如果没提供,就使用当前活动条目的下一个图片条目
  6. var $next = next || $active[type]()
  7. var isCycling = this.interval
  8.  
  9. // 获取移动的方向:如果是next,则是向左移动,否则是向右移动
  10. var direction = type == 'next' ? 'left' : 'right'
  11.  
  12. // 如果获取失败,指定一个元素进行特殊处理,如果再传next,则指向下一轮的图片
  13. // 即如果最后一个图片显示以后,还要next,那就是下一轮的first
  14. var fallback = type == 'next' ? 'first' : 'last'
  15. var that = this // 获取当前调用者的this对象,防止作用域污染
  16.  
  17. if (!$next.length) { // 如果下一个对象不存在
  18. if (!this.options.wrap) return // 判断wrap是否为假,如果是,则直接返回
  19. $next = this.$element.find('.item')[fallback]()
  20. // 否则,使用fallback指定的元素当做 $next对象元素
  21. }
  22. // 如果下一个元素已经是高亮了,则设置轮播标记为false
  23. if ($next.hasClass('active')) return this.sliding = false
  24.  
  25. // 设定轮播后要触发的事件,以及要暴露的参数
  26. var e = $.Event('slide.bs.carousel', { relatedTarget: $next[0], direction: direction })
  27. this.$element.trigger(e) // 触发slide事件
  28. if (e.isDefaultPrevented()) return // 如果要轮播的对象已经是active高亮了,直接返回不做处理
  29.  
  30. this.sliding = true // 标记轮播正在进行
  31. isCycling && this.pause() // 如果有间隔,则暂停自动执行
  32.  
  33. // 处理小圆圈的高亮状态
  34. if (this.$indicators.length) { // 如果有小圆圈指示符
  35. this.$indicators.find('.active').removeClass('active')
  36. // 去除原来高亮指示符的active样式
  37. // 设置一次性slid事件,以便在轮播后执行该事件,从而设置高亮指示符
  38. this.$element.one('slid.bs.carousel', function () {
  39. var $nextIndicator = $(that.$indicators.children()[that.getActiveIndex()])
  40. // 获取当前高亮图片的索引,按照该索引找到对的指示符
  41. $nextIndicator && $nextIndicator.addClass('active')
  42. // 如果找到的话,就添加active样式使其高亮
  43. })
  44. }
  45. // 如果支持动画,并且设置了slide样式(注意,这里不是fade效果)
  46. if ($.support.transition && this.$element.hasClass('slide')) {
  47. $next.addClass(type) // 给要轮播的元素添加type类型样式(比如:next、prev)
  48. $next[0].offsetWidth // 重绘UI
  49. $active.addClass(direction) // 给当前活动的对象添加方法(如left、right)
  50. $next.addClass(direction) // 给要轮播的元素添加方法(如left、right)
  51.  
  52. // 给当前活动元素绑定一次性动画事件,在该事件回调里执行以下操作
  53. $active
  54. .one($.support.transition.end, function () {
  55. // 在将要轮播的元素上,删除对应type和方向样式(如next left或者prev right),
  56. // 然后添加active样式
  57. $next.removeClass([type, direction].join(' ')).addClass('active')
  58. // 删除当前活动元素(即将隐藏)上的active样式和方向样式(如left或right)
  59. $active.removeClass(['active', direction].join(' '))
  60. that.sliding = false // 设置轮播状态结束
  61. // 然后触发slid事件,这里使用了setTimeout是确保UI刷新线程不被阻塞
  62. setTimeout(function () { that.$element.trigger('slid.bs.carousel') }, 0)
  63. })
  64. .emulateTransitionEnd($active.css('transition-duration').slice(0, -1) * 1000)
  65. } else { // 如果不支持动画
  66. $active.removeClass('active') // 删除当前高亮元素上的active样式
  67. $next.addClass('active') // 给要轮播的元素上添加高亮active样式
  68. this.sliding = false // 设置轮播状态结束
  69. this.$element.trigger('slid.bs.carousel') // 触发slid事件
  70. }
  71. isCycling && this.cycle() // 如果有间隔,则重新开始(间隔后)自动执行
  72. return this // 返回this,以便链式操作(这里的this是data-ride="carousel"容器元素)
  73. }

根据上述步骤可以得知,前面几行代码都是处理方向的,然后在执行的时候先判断是否支持动画(以及有否slide样式),如果不支持就直接把原来图片元素上的active删除,在新显示图片上加上active样式即可;而如果支持动画,则明显很复杂。我们来用下面的HTML运行结果来分析,首先B处于active状态,HTML代码如下:

  1. <div class="item"><img alt="A图" src="A" /></div>
  2. <div class="item active"><img alt="B图" src="B" /></div>
  3. <div class="item"><img alt="C图" src="C" /></div>

在触发slide.bs.carousel事件后,如果不阻止默认工作,就执行如下操作:

  1. $next.addClass(type) // 给要轮播的元素添加type类型样式(比如:next、prev)
  2. $next[0].offsetWidth // 重绘UI
  3. $active.addClass(direction) // 给当前活动的对象添加方法(如left、right)
  4. $next.addClass(direction) // 给要轮播的元素添加方法(如left、right)

也就是,首先给元素C添加一个next样式,此时是第一次变化。

  1. <div class="item"><img alt="A图" src="A" /></div>
  2. <div class="item active"><img alt="B图" src="B" /></div>
  3. <div class="item next"><img alt="C图" src="C" /></div>

然后通过调用$next[0].offsetWidth,重绘UI,以便能够生效,再给元素C添加left样式,并且也给原来的图片B也添加一个left样式,此时HTML结构变成如下这样(此时是第二次变化):

  1. <div class="item"><img alt="A图" src="A" /></div>
  2. <div class="item active left"><img alt="B图" src="B" /></div>
  3. <div class="item next left"><img alt="C图" src="C" /></div>

此时,我们再来看一下CSS里所定义的相关样式(我们这里只看next和left相关的,如果是反方向的prev和right相关的,请自行查看CSS源码)。

  1. // 源码5450行
  2. .carousel-inner &gt; .next,
  3. .carousel-inner &gt; .prev { /*如果是即将轮播的元素*/
  4. position: absolute; /*绝对定位*/
  5. top: 0; /*顶部间距为0*/
  6. width: 100%;
  7. }
  8. .carousel-inner &gt; .next { /*如果是下一张图片要显示*/
  9. left: 100%; /*则left为100%,表示在当前显示图片的右边,利用上面的overflow:
  10.  hidden;先隐藏起来*/
  11. }
  12. .carousel-inner &gt; .prev { /*如果上一张图片即将显示了*/
  13. left: -100%; /*则left为-100%,表示在当前显示图片的左边,利用上面的overflow:
  14.  hidden;先隐藏起来*/
  15. }
  16. .carousel-inner &gt; .next.left,
  17. .carousel-inner &gt; .prev.right { /*如果下一张图片即将显示了*/
  18. left: 0; /*则设置左对齐,然后利用动画滚动到左边*/
  19. }
  20. .carousel-inner &gt; .active.left { /*如果滚动方向是向左*/
  21. left: -100%; /*则上一张高亮显示的照片向左移动100%的距离,以便隐藏起来*/
  22. }
  23. .carousel-inner &gt; .active.right { /*如果滚动方向是向右*/
  24. left: 100%; /*则上一张高亮显示的照片向右移动100%的距离,以便隐藏起来*/
  25. }

在重绘UI线程之前,元素C的left值是100%;再次添加left样式后,left值变成了0,而原来的元素B的left值则变成了-100%,表示已经移出显示区域了。

与此同时,定义一个一次性动画事件,并在600毫秒以后执行。

  1. $active.one($.support.transition.end, function () {
  2. // 给当前活动元素绑定一次性动画事件,在该事件回调里执行如下操作
  3. $next.removeClass([type, direction].join(' ')).addClass('active')
  4. // 在将要轮播元素上,删除对应type和方向的样式(如next left或者prev right),然后添加active样式
  5. $active.removeClass(['active', direction].join(' '))
  6. // 删除当前活动元素(即将隐藏)上的active样式和方向样式(如left或right)
  7. that.sliding = false // 设置轮播状态结束
  8. setTimeout(function () { that.$element.trigger('slid') }, 0)
  9. // 然后触发slid事件,这里使用了setTimeout是确保UI刷新线程不被阻塞
  10. }).emulateTransitionEnd(600)

在回调函数里,我们可以看出,首先删除了元素C上的next和left样式,添加了active样式;然后删除了元素B上的active和left样式;最后设置轮播状态结束,触发slid事件。其结果HTML结构如下(最终状态):

  1. <div class="item"><img alt="A图" src="A" /></div>
  2. <div class="item "><img alt="B图" src="B" /></div>
  3. <div class="item active "><img alt="C图" src="C" /></div>

和初始HTML代码相比,其实就是把active样式从B的div上,拖到了C的div上,就这么简单。当然,有的人可能还会问到为什么最后一步触发slid事件用了setTimeout函数。

  1. setTimeout(function () { that.$element.trigger('slid') }, 0)
  2. // 然后触发slid事件,这里使用了setTimeout是确保UI刷新线程不被阻塞

这主要是因为,在默认情况下,浏览器上执行的JavaScript代码和UI更新是属于同一个现场,所以如果直接触发slid的话,可能会导致阻塞UI更新。UI线程的阻塞很多时候是由于我们要在代码里进行长时间的脚本运算,超过了浏览器限制,导致浏览器失去响应,造成假死的状态。使用setTimeout则表示,暂时放开线程的控制器,以便让UI能够立即开始更新,然后再执行setTimeout内部的代码,从而达到平滑的效果。

Slide方法分析完了,理解以后,大家可能就觉得真不错(或者是:也就这么回事嘛)。是的,正是这么简单的代码才构成了这么炫的特效。

步骤3 jQuery插件定义。jQuery插件的定义和其他插件类似。这里我们需要注意一下,当option传入为数字(number)的时候,是表示直接显示特定的图片,也就是调用该实例的.to(number)方法。源码如下:

  1. var old = $.fn.carousel
  2. // 保留其他库的$.fn.carousel代码(如果定义的话),以便在noConflict之后,可以继续使用该老代码
  3. $.fn.carousel = function (option) {
  4. return this.each(function () { // 遍历所有符合规则的元素
  5. var $this = $(this) // 当前触发元素的jQuery对象
  6. var data = $this.data('bs.carousel')
  7. // 获取自定义属性data-bs.carousel的值 (其实是
  8. // carousel实例)
  9.  
  10. // 合并参数,优先级依次递增
  11. // var options = $.extend({}, Carousel.DEFAULTS, $this.data(), typeof option ==
  12. // 'object' && option)
  13. // 如果option参数是字符串,直接使用,否则使用options里的slide参数
  14. var action = typeof option == 'string' ? option : options.slide
  15. // 如果没有carousel实例,就初始化一个,并传入this和参数
  16. if (!data) $this.data('bs.carousel', (data = new Carousel(this, options)))
  17.  
  18. // 如果option是数字,表示是想直接切换到某张图上,所以直接使用.to()方法
  19. if (typeof option == 'number') data.to(option)
  20. else if (action) data[action]() // 否则,再判断如果action存在,就执行action
  21. // 所对应的方法
  22. else if (options.interval) data.pause().cycle()
  23. // 最后,如果指定了interval参数,先暂停然后重新循环
  24. })
  25. }
  26. $.fn.carousel.Constructor = Carousel // 并重设插件构造器,可以通过该属性获取插件的真实类函数

步骤4 防冲突处理。此步骤与Modal插件的步骤4一样,此处不赘述。

步骤5 绑定触发事件。该步骤的触发代码比较复杂,主要是和其他的插件有所不同,该插件在触发元素上绑定事件,但并没有将触发元素作为Carousel类的实例。具体如下:

  1. // 绑定触发事件
  2. // 在带有data-slide或data-slide-to属性的元素上绑定事件
  3. $(document).on('click.bs.carousel.data-api', '[data-slide], [data-slide-to]', function (e) {
  4. var $this = $(this), href
  5.  
  6. // 查找target,即所指定的折叠地区的id或者选择符,如果没有target,就使用href里的值
  7. var $target = $($this.attr('data-target') || (href = $this.attr('href')) &&
  8. href.replace(/.*(?=#[^\s]+$)/, '')) // strip for ie7
  9.  
  10. // 合并target上的data-属性和触发元素上的data-属性
  11. var options = $.extend({}, $target.data(), $this.data())
  12.  
  13. // 查找单击元素上是否有data-slide-to属性
  14. // 如果存在,则取消间隔设置(因为单击data-slide-to意味着是手动触发行为,后续是不会循环播放的)
  15. var slideIndex = $this.attr('data-slide-to')
  16. if (slideIndex) options.interval = false
  17.  
  18. $target.carousel(options) // 实例化插件
  19.  
  20. // 再次判断如果单击的是小圆圈data-slide-to,
  21. // 则直接跳转到那张图上($target.data('bs.carousel')是绑定的插件实例)
  22. if (slideIndex = $this.attr('data-slide-to')) {
  23. $target.data('bs.carousel').to(slideIndex)
  24. }
  25. e.preventDefault() // 阻止默认行为
  26. })
  27.  
  28. $(window).on('load', function () {
  29. $('[data-ride="carousel"]').each(function () { // 遍历所有符合规则的元素
  30. var $carousel = $(this)
  31. $carousel.carousel($carousel.data())// 实例化插件(并收集data-参数),以便自动运行
  32. })
  33. })

通过上述代码可以看到,该触发代码有两部分。第一部分是对左右控制链接和圆圈指示符(也就是带data-slide、data-slide-to属性的元素)绑定click事件;并且判断如果单击了data-slide-to,则直接显示特定的图片。这里有一点需要注意的是:一旦选择了特定图片,options.interval参数就会被设置为false,也就是不自动轮播,但这取决于初始的轮播是否设置了interval参数,如果设置的话,则还是会自动轮播;如果没有设置,在显示指定的图片之后就会停止。第二部分是通用的插件初始化代码,就不多说了。