bootstrap之scrollspy

一、前言

這節介紹下scrollspy(滾動偵測)模塊的源碼實現。

二、源碼

 /* ========================================================================
  * Bootstrap: scrollspy.js v3.3.7(滾動偵測)
  * http://getbootstrap.com/javascript/#scrollspy
  * ========================================================================
  * Copyright 2011-2016 Twitter, Inc.
  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
  * ======================================================================== */
 +function ($) {
   'use strict';

   // SCROLLSPY CLASS DEFINITION(構造函數,初始化要監聽的滾動對象、nav區(selector),執行滾動事件綁定)
   // ==========================
   function ScrollSpy(element, options) {
     this.$body          = $(document.body)
     this.$scrollElement = $(element).is(document.body) ? $(window) : $(element)
     this.options        = $.extend({}, ScrollSpy.DEFAULTS, options)
     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()
   }

   ScrollSpy.VERSION  = '3.3.7'
   ScrollSpy.DEFAULTS = {
     offset: 10
   }

   // 獲取指定元素的scrollHeight(內容高度),或兼容獲取body的內容高度
   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)
   }

   // 獲得content區中對應的錨點(放到targets中)和對應的高度(放到offsets中),及獲取滾動容器的內容高度
   ScrollSpy.prototype.refresh = function () {
     var that          = this
     var offsetMethod  = 'offset'
     // offsetBase爲滾動容器data-spy="scroll"相對於滾動條頂部的偏移
     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()
     }

     // 找到全部的錨點,返回由[offsets,錨點]組成的數組
     // jquery的map有點奇怪,return值或return[值]得到的都是數組。return [[值]]得到的纔是數組組成的數組
     this.$body
       .find(this.selector)
       .map(function () {
         var $el   = $(this)
         // 獲取符合格式的錨點$href
         var href  = $el.data('target') || $el.attr('href')
         var $href = /^#./.test(href) && $(href)

         // $href[offsetMethod]().top => 相對於父元素(即滾動容器data-spy="scroll")的偏移量
         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])
       })
   }

   // 根據this.offsets與當前的scrollTop比較,判斷是否需要activate
   ScrollSpy.prototype.process = function () {
     // 加上規定offset的,距離頂部的值(this.options.offset:當計算滾動位置時,距離頂部的偏移像素)
     var scrollTop    = this.$scrollElement.scrollTop() + this.options.offset
     // 當前的內容高度
     var scrollHeight = this.getScrollHeight()
     // offset值+內容高度-可視高度得到的最大可滾動高度
     var maxScroll    = this.options.offset + scrollHeight - this.$scrollElement.height()
     var offsets      = this.offsets
     var targets      = this.targets
     var activeTarget = this.activeTarget
     var i

     // 當內容高度發生動態變化時調用refresh方法
     if (this.scrollHeight != scrollHeight) {
       this.refresh()
     }

     // 超過或等於當前元素的最大可滾動高度,說明滾動到了最底部,直接激活最後一個nav
     if (scrollTop >= maxScroll) {
       return activeTarget != (i = targets[targets.length - 1]) && this.activate(i)
     }

     // 沒超過第一個offset,清除當前的激活對象(當this.options.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
     }
   }

   // 激活傳進來的dom對象(即爲其添加active類)
   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')

     // 爲下拉菜單的相應li元素添加active類
     if (active.parent('.dropdown-menu').length) {
       active = active
         .closest('li.dropdown')
         .addClass('active')
     }

     // 觸發自定義事件
     active.trigger('activate.bs.scrollspy')
   }

   // 清除當前的nav中的激活對象
   ScrollSpy.prototype.clear = function () {
     $(this.selector)
       .parentsUntil(this.options.target, '.active')
       .removeClass('active')
   }

   // SCROLLSPY PLUGIN DEFINITION
   // ===========================
   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]()
     })
   }

   var old = $.fn.scrollspy

   $.fn.scrollspy             = Plugin
   $.fn.scrollspy.Constructor = ScrollSpy

   // SCROLLSPY NO CONFLICT
   // =====================
   $.fn.scrollspy.noConflict = function () {
     $.fn.scrollspy = old
     return this
   }

   // SCROLLSPY DATA-API
   // ==================
   $(window).on('load.bs.scrollspy.data-api', function () {
     $('[data-spy="scroll"]').each(function () {
       var $spy = $(this)
       Plugin.call($spy, $spy.data())
     })
   })
 }(jQuery);

三、應用 & 源碼分析

1、應用

 <nav id="navbar-example" class="navbar navbar-default navbar-static" role="navigation">
   <div class="container-fluid">
     <div class="navbar-header">
       <button class="navbar-toggle" type="button" data-toggle="collapse"
               data-target=".bs-js-navbar-scrollspy">
         <span class="icon-bar"></span><span class="icon-bar"></span><span class="icon-bar"></span>
       </button>
       <a class="navbar-brand" href="#">教程名稱</a>
     </div>
     <div class="collapse navbar-collapse bs-js-navbar-scrollspy">
       <ul class="nav navbar-nav">
         <li><a href="#ios">iOS</a></li>
         <li><a href="#svn">SVN</a></li>
         <li class="dropdown">
           <a href="#" id="navbarDrop1" class="dropdown-toggle"
              data-toggle="dropdown">Java
             <b class="caret"></b>
           </a>
           <ul class="dropdown-menu" role="menu"
               aria-labelledby="navbarDrop1">
             <li><a href="#jmeter" tabindex="-1">jmeter</a></li>
             <li><a href="#ejb" tabindex="-1">ejb</a></li>
             <li class="divider"></li>
             <li><a href="#spring" tabindex="-1">spring</a></li>
           </ul>
         </li>
       </ul>
     </div>
   </div>
 </nav>
 <div data-spy="scroll" data-target="#navbar-example" data-offset="-10"
      style="height:200px;overflow:auto; position: relative;">
   <h4 id="ios">iOS</h4>
   <p>iOS 是一個由蘋果公司開發和發佈的手機操作系統。最初是於 2007 年首次發佈 iPhone、iPod Touch 和 Apple
     TV。iOS 派生自 OS X,它們共享 Darwin 基礎。OS X 操作系統是用在蘋果電腦上,iOS 是蘋果的移動版本。
   </p>
   <h4 id="svn">SVN</h4>
   <p>Apache Subversion,通常縮寫爲 SVN,是一款開源的版本控制系統軟件。Subversion 由 CollabNet 公司在 2000 年創建。但是現在它已經發展爲 Apache Software
     Foundation 的一個項目,因此擁有豐富的開發人員和用戶社區。
   </p>
   <h4 id="jmeter">jMeter</h4>
   <p>jMeter 是一款開源的測試軟件。它是 100% 純 Java 應用程序,用於負載和性能測試。
   </p>
   <h4 id="ejb">EJB</h4>
   <p>Enterprise Java Beans(EJB)是一個創建高度可擴展性和強大企業級應用程序的開發架構,部署在兼容應用程序服務器(比如 JBOSS、Web Logic 等)的 J2EE 上。
   </p>
   <h4 id="spring">Spring</h4>
   <p>Spring 框架是一個開源的 Java 平臺,爲快速開發功能強大的 Java 應用程序提供了完備的基礎設施支持。
   </p>
   <p>Spring 框架最初是由 Rod Johnson 編寫的,在 2003 年 6 月首次發佈於 Apache 2.0 許可證下。
   </p>
 </div>

2、源碼分析

1、refresh
獲取導航元素 nav 內各鏈接對應元素的相對高度(如果不是 body,則相對於父元素),然後存儲各錨點和相對高度
2、process
遍歷錨點,判斷錨點對應元素的相對高度是否位於錨點之間,然後’激活’對應錨點

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章