一、前言
這節介紹下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
遍歷錨點,判斷錨點對應元素的相對高度是否位於錨點之間,然後’激活’對應錨點