5.12.3 源码分析

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

步骤2 插件核心代码。通过对Affix插件的实现原理分析,结合其他一些插件的源码分析,我们可以看出,该插件在其他地方大同小异,唯一有难度的就是如何做到实时监控滚动,以及如何实时计算设置的offset值,以便对affix-top、affix以及affix-bottom样式进行及时的切换。只有及时切换,才能控制相应的滚动/固定状态。先来看一下摘要代码:

  1. // 定义Affix类
  2. var Affix = function (element, options) {
  3. this.options = $.extend({}, Affix.DEFAULTS, options) // 合并参数,options优先级高于默认值
  4. this.$window = $(window) // 顶级对象window上监控scroll和click事件
  5. // scroll事件发生时,调用checkPosition方法
  6. .on('scroll.bs.affix.data-api', $.proxy(this.checkPosition, this))
  7. // click事件发生时,调用checkPositionWithEventLoop方法
  8. .on('click.bs.affix.data-api', $.proxy(this.checkPositionWithEventLoop, this))
  9.  
  10. this.$element = $(element) // 要固定粘住的元素
  11. this.affixed =
  12. this.unpin =
  13. this.pinnedOffset = null
  14.  
  15. this.checkPosition() // 默认调用一次,初始化一下位置
  16. }
  17.  
  18. Affix.RESET = 'affix affix-top affix-bottom'
  19. Affix.DEFAULTS = { offset: 0 }
  20. // 获取固定定位元素的offset
  21. Affix.prototype.getPinnedOffset = function () {}
  22. // click事件时,调用此方法调整位置
  23. Affix.prototype.checkPositionWithEventLoop = function () {}
  24. // 重新计算位置的方法
  25. Affix.prototype.checkPosition = function (){};

通过上面的代码可见,checkPositionWithEventLoop原型方法最终还是调用了checkPosition原型方法,所以checkPosition方法是最核心的内容,所有计算高度margin-top、padding-top的代码都在该函数里。代码和注释如下:

  1. // 获取固定定位元素的offset
  2. Affix.prototype.getPinnedOffset = function () {
  3. if (this.pinnedOffset) return this.pinnedOffset
  4. this.$element.removeClass(Affix.RESET).addClass('affix')
  5. var scrollTop = this.$window.scrollTop()
  6. var position = this.$element.offset()
  7. return (this.pinnedOffset = position.top - scrollTop)
  8. }
  9. // click事件时,调用此方法调整位置
  10. Affix.prototype.checkPositionWithEventLoop = function () {
  11. // 使用setTimeout的目的,是让事件循环都处理结束(1毫秒)后,才调用checkPosition
  12. setTimeout($.proxy(this.checkPosition, this), 1)
  13. }
  14. // 重新计算位置的方法
  15. Affix.prototype.checkPosition = function () {
  16. if (!this.$element.is(':visible')) return // 如果元素不可见的话,直接返回
  17.  
  18. var scrollHeight = $(document).height() // 整个文档的高度
  19. var scrollTop = this.$window.scrollTop() // 窗口向上滚动的偏移量(单位像素)
  20. var position = this.$element.offset() // 返回该元素相对滚动条顶部的偏移量(单位像素)
  21. var offset = this.options.offset // 默认的偏移量设置
  22. var offsetTop = offset.top // 顶部top的偏移量设置
  23. var offsetBottom = offset.bottom // 底部bottom的偏移量设置
  24.  
  25. // 判断如果affix形式是top,则将scrollTop加到原来的top上
  26. if (this.affixed == 'top') position.top += scrollTop
  27.  
  28. // 因为offset支持不同的方式传值,所以需要判断它是数字还是对象或函数
  29. // 如果offset不是对象,则表明是一个数字,则将offset赋值于offsetBottom和offsetTop
  30. if (typeof offset != 'object') offsetBottom = offsetTop = offset
  31. // 如果offsetTop是函数,就将其执行结果赋值给offsetTop
  32. if (typeof offsetTop == 'function') offsetTop = offset.top(this.$element)
  33. // 如果offsetBottom是函数,就将其执行结果赋值给offsetBottom
  34. if (typeof offsetBottom == 'function') offsetBottom = offset.bottom(this.$element)
  35.  
  36. // 计算affix当前应该属于什么状态?top、正常、bottom(如果看不明白,下面有改造后的if/else代码)
  37. var affix = this.unpin != null && (scrollTop + this.unpin <= position.top) ? false :
  38. offsetBottom != null && (position.top + this.$element.height() >=
  39. scrollHeight - offsetBottom) ? 'bottom' :
  40. offsetTop != null && (scrollTop <= offsetTop) ? 'top' : false
  41.  
  42. if (this.affixed === affix) return // 如果原来的状态和现在计算的状态一致的话,就不需要处理了
  43. if (this.unpin) this.$element.css('top', '') // 如果为unpin,就清空top值
  44.  
  45. var affixType = 'affix' + (affix ? '-' + affix : '') // 判断affix类型
  46. var e = $.Event(affixType + '.bs.affix') // 设置要触发的affix事件
  47.  
  48. this.$element.trigger(e) // 触发affix事件
  49.  
  50. if (e.isDefaultPrevented()) return
  51.  
  52. this.affixed = affix // 将最新的affix状态赋值给affixed
  53. // 如果是bottom模式,则通过getPinnedOffset获取
  54. this.unpin = affix == 'bottom' ? this.getPinnedOffset() : null
  55. this.$element
  56. .removeClass(Affix.RESET) // 删除所有的affix样式
  57. .addClass(affixType) // 再添加最新的样式,如果affix模式不为空,
  58. // 则添加两个样式,如affix或affix-bottom
  59. .trigger($.Event(affixType.replace('affix', 'affixed')))
  60. // 根据类型,触发相应的affixed事件
  61.  
  62. if (affix == 'bottom') { // 如果是bottom模式,则重新设置元素offset里的top值
  63. this.$element.offset({ top: scrollHeight - offsetBottom - this.
  64. $element.height() })
  65. }
  66. }

上述代码中,最复杂的就是连续嵌套的三目表达式。为了更容易理解,改成if/else代码再来看一下:

  1. var affix;
  2. // 如果unpin不为空,计算(屏幕滚动的高度+unpin),如果其和小于affix元素的top值,则表示不需
  3. 要固定位置
  4. if (this.unpin != null && (scrollTop + this.unpin <= position.top)) {
  5. affix = false;
  6. }
  7. else {
  8. // 如果offsetBottom不为空,并且(元素的top值+元素的高度)>=(滚动高度-offsetBottom)
  9. if (offsetBottom != null && (position.top + this.$element.height() >=
  10. scrollHeight - offsetBottom)) {
  11. // 则表示affix模式为bottom
  12. affix = "bottom";
  13. } else {
  14. // 如果offsetTop不为空,如果(滚动高度)<=(设置的offsetTop),则表示affix模式为top
  15. // (正常模式)
  16. if (offsetTop != null && (scrollTop <= offsetTop)) {
  17. affix = "top";
  18. }
  19. else {
  20. affix = false;
  21. }
  22. }
  23. }

这样看起来是不是好理解一点?如果还不理解,请结合前面的原理示意图(图5-22),再理解一遍。看的时候最好也写代码自己练习一下。

步骤3 jQuery插件定义。Affix插件在jQuery上的定义和其他插件没有什么不同。

  1. // 在jQuery上定义affix插件,并重设插件构造器
  2. var old = $.fn.affix
  3. // 保留其他库的$.fn.affix代码(如果定义的话),以便在noConflict之后,可以继续使用该老代码
  4. $.fn.affix = function (option) {
  5. return this.each(function () { // 遍历所有符合规则的元素
  6. var $this = $(this) // 当前触发元素的jQuery对象
  7. var data = $this.data('bs.affix') // 获取自定义属性data-bs.affix的值(其实是affix实例)
  8. var options = typeof option == 'object' && option // 合并参数
  9.  
  10. // 如果没有Affix实例,就初始化一个,并传入this和参数
  11. if (!data) $this.data('bs.affix', (data = new Affix(this, options)))
  12.  
  13. // 如果option是字符串,则表示直接调用该实例上的同名方法
  14. if (typeof option == 'string') data[option]()
  15. })
  16. }
  17. $.fn.affix.Constructor = Affix // 并重设插件构造器,可以通过该属性获取插件的真实类函数

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

步骤5 绑定触发事件。绑定触发事件的源码如下:

  1. // 绑定触发事件
  2. $(window).on('load', function () {
  3. $('[data-spy="affix"]').each(function () { // 遍历所有符合规则的元素
  4. var $spy = $(this) // 临时赋值变量
  5. var data = $spy.data() // 收集该元素上的自定义属性(data-开头)
  6.  
  7. data.offset = data.offset || {} // 如果设置了offset就使用它,否则传一个默认空值
  8.  
  9. // 如果设置了data-offset-bottom属性,则将它的值赋给data.offset.bottom
  10. if (data.offsetBottom) data.offset.bottom = data.offsetBottom
  11.  
  12. // 如果设置了data-offset-top属性,则将它的值赋给data.offset.top
  13. if (data.offsetTop) data.offset.top = data.offsetTop
  14. $spy.affix(data) // 实例化插件(并收集data-参数),以便自动运行
  15. })
  16. })

通过上述代码可以看到,上述代码offset做了特殊处理,即先检测自定义属性data-offset,临时保存一下;然后再判断有没有data-offset-top(或data-offset-bottom),如果有,就使用它,如果没有,就使用普通的offset。也就是说了,如果同时声明了data-offset=100和data-offset-top=60,最终的结果就是:top用60,bottom用100。