使用方法
可以分为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>
核心思路
- 页面加载完成后为所有data-spy="scroll"元素都实例化一个ScrollSpy实例。即绑定滚动事件,找到导航条(selector),对应的锚点及锚点的offsets
- 在滚动时,先判断滚动高度是否大于最大的滚动高度,若是则高亮最后一个导航条;再判断滚动高度是否小于最小offset,若是则清除高亮;最后若小于最大滚动高度,大于最小滚动高度,用三个&&判断是在哪一段offset中,再高亮对应的导航条
- 清除当前高亮的导航条,再高亮现在对应的导航条
- 而当导航条为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()
}
/**
* 获取要监听的元素的内容高度,或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)
}
/**
* 确定各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])
})
}
/**
* 根据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
}
}
/**
* 激活传进来的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')
}
/**
* 清除当前的nav中的激活对象
* @return {[type]} [description]
*/
ScrollSpy.prototype.clear = function () {
$(this.selector)
.parentsUntil(this.options.target, '.active')
.removeClass('active')
}
总结
- $el.data()能获得元素上的data缓存数据的对象
- 利用Math.max(document.body.scrollHeigth, document.documentElement.scrollHeight)兼容性获得元素的内容高度
- jquery的offset()可以获得元素在当前视口的相对偏移(在body监听滚动时用),position()获取元素相对其父元素的偏移(在元素内监听滚动时用)
- jquery的map()的奇怪表现
- 利用for循环加3个&&就可以判断滚动高度是否满足条件