当前位置: 首页 > 工具软件 > Scrollspy > 使用案例 >

Bootstrap插件scrollspy源码的学习

萧和同
2023-12-01

使用方法

可以分为3部分:要监听滚动的元素、包含a标签的容器、包含对应锚点的content
  • 在要监听滚动的元素上加data-spy="scroll" data-target="#example"(这里只要是对应的类或id均可)。这些值会在初始化Plugin时作为option对象的属性被传入ScrollSpy中
  • 包含a标签的容器必须有与data-target对应的类或id。而a标签要有data-target或href来指定对应的content中的锚点的id
  • content中各锚点要有对应的id,与a标签关联
<body data-spy="scroll" data-target="#example">
  <!-- 与要监听的标签中的target对应,且必须在"nav"类的层级之上 -->
  <div id="example">
    <ul class="nav">
      <li><a href="#item1">@item1</a></li>
      <li><a href="#item2">@item2</a></li>
      <li><a href="#item3">@item3</a></li>
    </ul>
  </div>
  <h2 id="item1">@item1</h2>
  <h2 id="item2">@item2</h2>
  <h2 id="item3">@item3</h2>
</body>

核心思路

  1. 页面加载完成后为所有data-spy="scroll"元素都实例化一个ScrollSpy实例。即绑定滚动事件,找到导航条(selector),对应的锚点及锚点的offsets
  2. 在滚动时,先判断滚动高度是否大于最大的滚动高度,若是则高亮最后一个导航条;再判断滚动高度是否小于最小offset,若是则清除高亮;最后若小于最大滚动高度,大于最小滚动高度,用三个&&判断是在哪一段offset中,再高亮对应的导航条
  3. 清除当前高亮的导航条,再高亮现在对应的导航条
  4. 而当导航条为a标签时,href因为指向id,所以点击导航条会自动定位到对应Content

初始化

在onload之后,给每个含有data-spy属性的标签,完成Plugin的初始化
$(window).on('load.bs.scrollspy.data-api', function () {
  $('[data-spy="scroll"]').each(function () {
    var $spy = $(this)
    Plugin.call($spy, $spy.data())
  })
})

Plugin入口分析

/**
 * 把[data-spy="scroll"]的dom元素,及元素property上的参数传给ScrollSpy构造函数
 * @param {object} option 之前绑定在元素上的property对象,含有spy和target
 *                        自定义的包含指定content的对象,如{target: '#nav-example'}
 */
function Plugin(option) {
  return this.each(function () {
    var $this   = $(this)
    var data    = $this.data('bs.scrollspy')
    var options = typeof option == 'object' && option

    if (!data) $this.data('bs.scrollspy', (data = new ScrollSpy(this, options)))
    if (typeof option == 'string') data[option]()
  })
}

结构分析

// 构造函数,初始化要监听的滚动对象、nav区(selector),执行滚动事件绑定
function ScrollSply(element, options){};
// 获取指定元素的scrollHeigth(内容高度),或兼容获取body的内容高度
ScrollSpy.prototype.getScrollHeight = function () {};
// 获得content区中对应的锚点和对应的高度
ScrollSpy.prototype.refresh = function () {};
// 是否激活的逻辑判断
ScrollSpy.prototype.process = function () {};
// 激活传入的target的父元素li
ScrollSpy.prototype.activate = function (target) {};
// 清除当前的激活对象
ScrollSpy.prototype.clear = function () {};

具体分析

  • 构造函数
/**
 * 完成滚动对象、nav对象、滚动事件绑定、初始化refresh()和process()
 * @param {object} element 含有[data-spy="scroll"]的dom对象
 * @param {object} options 定义offsets的参数
 */
function ScrollSpy(element, options) {
  this.$body          = $(document.body)
  // 滚动对象,如果绑定在body上,则滚动对象为window,否则为绑定的元素
  this.$scrollElement = $(element).is(document.body) ? $(window) : $(element) 
  this.options        = $.extend({}, ScrollSpy.DEFAULTS, options)
  // 对应的target(即nav)上的a标签作为selector
  // 因为是匹配'target .nav li > a',所以才会出现target不能和.nav同一层级
  this.selector       = (this.options.target || '') + ' .nav li > a'
  this.offsets        = []
  this.targets        = []
  this.activeTarget   = null
  this.scrollHeight   = 0

  this.$scrollElement.on('scroll.bs.scrollspy', $.proxy(this.process, this))
  this.refresh()
  this.process()
}
  • getScrollHeight方法
/**
 * 获取要监听的元素的内容高度,或body的内容高度
 * @return {number} 内容高度
 */
ScrollSpy.prototype.getScrollHeight = function () {
  // scrollHeigth是元素内容的高度,包括overflow导致不可见的部分
  // this.$body[0].scrollHeight和document.body.scrollHeight其实是一样的
  // 在DTD声明和未声明时,document.documentElement.scrollHeight和document.body.scrollHeight有一个会为可视窗口高度
  // 所以用Math.max取得全部内容高度
  return this.$scrollElement[0].scrollHeight || Math.max(this.$body[0].scrollHeight, document.documentElement.scrollHeight)
}
  • refresh方法
/**
 * 确定各a标签对应的锚点(放到targets中),及其位置(放到offsets中)
 * @return {undefined} 无返回值
 */
ScrollSpy.prototype.refresh = function () {
  var that          = this
  var offsetMethod  = 'offset'
  var offsetBase    = 0

  this.offsets      = []
  this.targets      = []
  this.scrollHeight = this.getScrollHeight()
  // 如果监听的滚动对象不是body,则使用position方法来获取offsets值
  // jquery的offset()方法是获取匹配元素在当前视口的相对偏移
  // position()方法是获取匹配元素相对父元素的偏移
  if (!$.isWindow(this.$scrollElement[0])) {
    offsetMethod = 'position'
    offsetBase   = this.$scrollElement.scrollTop()
  }

  this.$body
    // 找到全部的锚点
    .find(this.selector)
    .map(function () {
      var $el   = $(this)
      var href  = $el.data('target') || $el.attr('href')
      var $href = /^#./.test(href) && $(href)

      // 返回由[offsets,锚点]组成的数组
      // jquery的map有点奇怪,return值或return[值]得到的都是数组。return [[值]]得到的才是数组组成的数组
      return ($href 
        && $href.length
        && $href.is(':visible')
        && [[$href[offsetMethod]().top + offsetBase, href]]) || null
    })
    .sort(function (a, b) { return a[0] - b[0] })
    .each(function () {
      that.offsets.push(this[0])
      that.targets.push(this[1])
    })
}
  • process方法
/**
 * 根据this.offsets与当前的scrollTop比较,判断是否需要activate
 * @return {undefined} 没有返回值
 */
ScrollSpy.prototype.process = function () {
  var scrollTop    = this.$scrollElement.scrollTop() + this.options.offset // 加上规定offset的,距离顶部的值
  var scrollHeight = this.getScrollHeight() // 当前的内容高度
  var maxScroll    = this.options.offset + scrollHeight - this.$scrollElement.height() // offset值+内容高度-可视高度得到的最大可滚动高度
  var offsets      = this.offsets
  var targets      = this.targets
  var activeTarget = this.activeTarget // 当前的激活nav
  var i

  if (this.scrollHeight != scrollHeight) {
    this.refresh()
  }
  // 超过当前元素的最大可滚动高度,直接激活最后一个nav
  if (scrollTop >= maxScroll) {
    return activeTarget != (i = targets[targets.length - 1]) && this.activate(i)
  }
  // 没超过第一个offset,清除当前的激活对象
  if (activeTarget && scrollTop < offsets[0]) {
    this.activeTarget = null
    return this.clear()
  }
  // 最精彩的部分,循环判断是否需要激活
  for (i = offsets.length; i--;) {
    activeTarget != targets[i] // 满足当前遍历的target不是激活对象
      && scrollTop >= offsets[i] // 满足当前滚动高度大于对应的offset
      && (offsets[i + 1] === undefined || scrollTop < offsets[i + 1]) // 满足当前滚动高度小于下一个滚动高度,或下一个滚动高度未定义
      && this.activate(targets[i]) // 激活该nav
  }
}
  • activate方法
/**
 * 激活传进来的dom对象(即为其添加active类)
 * @param  {object} target dom对象
 * @return {undefined}        无返回值
 */
ScrollSpy.prototype.activate = function (target) {
  this.activeTarget = target // 先把该对象存入实例对象中

  this.clear() // 清除当前的激活对象

  var selector = this.selector +
    '[data-target="' + target + '"],' +
    this.selector + '[href="' + target + '"]'
  // 为对应的a标签的父元素li添加active类
  var active = $(selector)
    .parents('li')
    .addClass('active')

  if (active.parent('.dropdown-menu').length) {
    active = active
      .closest('li.dropdown')
      .addClass('active')
  }
  // 触发自定义事件
  active.trigger('activate.bs.scrollspy')
}
  • clear方法
/**
 * 清除当前的nav中的激活对象
 * @return {[type]} [description]
 */
ScrollSpy.prototype.clear = function () {
  $(this.selector)
    .parentsUntil(this.options.target, '.active')
    .removeClass('active')
}

总结

  1. $el.data()能获得元素上的data缓存数据的对象
  2. 利用Math.max(document.body.scrollHeigth, document.documentElement.scrollHeight)兼容性获得元素的内容高度
  3. jquery的offset()可以获得元素在当前视口的相对偏移(在body监听滚动时用),position()获取元素相对其父元素的偏移(在元素内监听滚动时用)
  4. jquery的map()的奇怪表现
  5. 利用for循环加3个&&就可以判断滚动高度是否满足条件







 类似资料: