
固定在底部的demo效果(对应sticky-bottom.html):

1. 实现思路
实现这个组件的关键在于找到元素何时被固定以及何时被取消固定的临界点,要找到这个临界点,首先要详细看看前面demo的变化过程。在前面的demo中,有一个导航条元素,也就是我们要控制固定与否的元素,我把它称为sticky元素;还有一个元素,它用来显示网页的一块列表内容,这个列表元素跟sticky元素在功能上是相关的,因为sticky元素要导航的正是这个列表元素提供的内容,本文在开始介绍sticky组件的功能时,就说过sticky组件固定是发生在网页滚动至某一区域的时候,离开这一区域就会取消固定,这个滚动区域或者说滚动范围,就是由列表元素来决定的,所以这个列表元素是找到临界点的关键,它表示sticky组件可被固定的网页滚动范围,为了后面引用方便,我把这个元素称为target元素。下面就来详细了解下前面demo的变化过程,由于固定在底部的情况与固定在顶部的情况实现思路是相通的,如果弄明白了固定在顶部的实现原理,相信你也一定能弄明白固定在底部的实现原理,所以这里也是为了减少篇幅,提高效率,仅仅介绍固定在顶部的情况:
一开始sticky元素和target元素的状态是这样的:

当滚动条慢慢向下,使得网页向上滚动的时候,sticky元素和target元素在一段滚动距离内状态并没有发生变化,一直到这个状态(滚动条滚动距离为573px):

在这个状态只要滚动条再往下滚动1px,sticky元素就会被固定在顶部(滚动条滚动距离为574px):

也就是说当target元素的顶部离浏览器顶部的距离小于0的时候(target元素的顶部未超出浏览器顶部的时候,距离看作大于0),sticky元素就会被固定,所以这就是我们要找的第一个临界点。然后滚动条继续向下滚动,只要target元素还在浏览器可视区域内,sticky元素就会一直被固定:

直到这个状态(滚动条滚动距离为1861px):

在这个状态只要滚动条再往下滚动1px,sticky元素就会取消固定在顶部(滚动条滚动距离为1862px):

显然,这就是我们要找的第2个临界点,不过它的判断条件是:当target元素的底部离浏览器顶部的距离小于sticky元素的高度时,sticky元素就会被取消固定。这里为什么是小于sticky元素的高度,而不是小于0,原因是因为基于小于0这个临界点开发出来的组件,会出现target元素几乎快从浏览器可视区域消失了,但是sticky元素还固定在那的效果:

sticky还把footer的内容给盖住了,本来是为了方便用户操作,结果影响了用户操作,所以得把取消固定这个临界点提前,而用sticky元素的高度最合适。
通过前面对demo变化过程的拆解,我们已经得到了滚动条一直向下滚动时,sticky状态变化的两个临界点:
1)当target元素的顶部离浏览器顶部的距离小于0的时候,sticky元素就会被固定;
2)当target元素的底部离浏览器顶部的距离小于sticky元素的高度时,sticky元素就会被取消固定。
综合这两个临界点,可以得出滚动条向下滚动时,sticky元素被固定的滚动范围的判断条件是:target元素的顶部离浏览器顶部的距离小于0 并且 target元素的底部离浏览器顶部的距离大于sticky元素的高度。而且这个判断条件,同样适用于滚动条向上滚动的情况,因为滚动条一直向上滚动时,sticky状态变化的临界点是:
1)当target元素的底部离浏览器顶部的距离大于sticky元素的高度时,sticky元素就会被固定;
2)当target元素的顶部离浏览器顶部的距离大于0的时候,sticky元素就会被取消固定。
(这两个临界点,其实跟滚动条向下滚动时提到的两个临界点,是一个意思,只不过是正话反着说而已)
所以只要得到【target元素的顶部离浏览器顶部的距离】,【target元素的底部离浏览器顶部的距离】,【sticky元素的高度】这三个值基本上就能实现这个组件了。这三个值中sticky元素的高度由设计图决定,它从网页一开始制作就是已知的,在定义组件的时候我们可以从外部传进去,虽然也能从js去获取它的高度,不过显然没有必要增加额外的计算;另外两个值【target元素的顶部离浏览器顶部的距离】,【target元素的底部离浏览器顶部的距离】,我们正好可以利用DOM提供的一个方法来获取,这个方法是:getBoundingClientRect,这是一个兼容性很好的方法,它的调用方式是:
var target = document.getElementById("main-container");var rect = target.getBoundingClientRect();console.log(rect);


top跟bottom恰恰就是我们要获取的【target元素的顶部离浏览器顶部的距离】,【target元素的底部离浏览器顶部的距离】,而且当框的顶部或底部未超出浏览器顶部的时候,top跟bottom都是大于0的值,而当框的顶部或底部超出浏览器顶部的时候,top跟bottom是小于0的值:

当我们找到了【target元素的顶部离浏览器顶部的距离】,【target元素的底部离浏览器顶部的距离】,【sticky元素的高度】这三个值,就可以用代码来描述前面的判断条件:
rect.top < 0 && (rect.bottom - stickyHeight) > 0;(rect表示target元素调用getBoundingClientRect返回的对象,stickyHeight表示sticky元素的高度)
var docClientWidth = document.documentElement.clientHeight;rect.bottom > docClientWidth && (rect.top + stickyHeight) < docClientWidth;2. 实现细节
<div class="container-fluid sticky-wrapper"><ul id="sticky" data-target="#main-container" class="sticky nav nav-pills"><li role="presentation" class="active"><a href="#">Home</a></li><li role="presentation"><a href="#">Profile</a></li><li role="presentation"><a href="#">Messages</a></li></ul></div><div id="main-container" class="container-fluid"><div class="row">...</div>...</div>固定在底部的html结构:
<div id="main-container" class="container-fluid"><div class="row">...</div>...</div><div class="container-fluid sticky-wrapper"><ul id="sticky" data-target="#main-container" class="sticky nav nav-pills"><li role="presentation" class="active"><a href="#">Home</a></li><li role="presentation"><a href="#">Profile</a></li><li role="presentation"><a href="#">Messages</a></li></ul></div>以上#main-container就是我们的target元素,#sticky就是我们的sticky元素,还需要注意两点:
.sticky-wrapper {margin-bottom: 10px;height: 52px;}这是因为当sticky元素被固定的时候,它会脱离普通文档流,所以要利用它的父元素把sticky元素的高度在普通文档流中撑起来,以免在固定效果出现的时候,target元素的内容出现跳动的情况。.sticky--in-top,.sticky--in-bottom {position: fixed;z-index: 1000;}.sticky--in-top {top: 0;}.sticky--in-bottom {bottom: 0;}当我们判断元素需要被固定在顶部的时候,就给它添加.sticky--in-top的css类;当我们判断元素需要被固定在底部的时候,就给它添加.sticky--in-bottom的css类。$(window).scroll(function() {var rect = $target[0].getBoundingClientRect();if (rect.top < 0 && (rect.bottom - stickyHeight) > 0) {!$elem.hasClass("sticky--in-top") && $elem.addClass("sticky--in-top").css("width", stickyWidth + "px");} else {$elem.hasClass("sticky--in-top") && $elem.removeClass("sticky--in-top").css("width", "auto");}});$(window).scroll(function() {var rect = $target[0].getBoundingClientRect(),docClientWidth = document.documentElement.clientHeight; if (rect.bottom > docClientWidth && (rect.top + stickyHeight) < docClientWidth) {!$elem.hasClass("sticky--in-bottom") && $elem.addClass("sticky--in-bottom").css("width", stickyWidth + "px");} else {$elem.hasClass("sticky--in-bottom") && $elem.removeClass("sticky--in-bottom").css("width", "auto");}});function throttle(func, wait) {var timer = null;return function() {var self = this,args = arguments;if (timer) clearTimeout(timer);timer = setTimeout(function() {return typeof func === "function" && func.apply(self, args);}, wait);}}$(window).scroll(throttle(function() {var rect = $target[0].getBoundingClientRect(),docClientWidth = document.documentElement.clientHeight; if (rect.bottom > docClientWidth && (rect.top + stickyHeight) < docClientWidth) {!$elem.hasClass("sticky--in-bottom") && $elem.addClass("sticky--in-bottom").css("width", stickyWidth + "px");} else {$elem.hasClass("sticky--in-bottom") && $elem.removeClass("sticky--in-bottom").css("width", "auto");}}, 50);其实真正处理回调的是throttle返回的函数,这个返回的函数逻辑少,而且没有DOM操作,它是会被连续调用的,但是不影响页面性能,而我们真正处理逻辑的那个函数,也就是传入throttle的那个函数因为throttle创建的闭包的作用,不会被连续调用,这样就实现了控制函数执行次数的目的。/*** @param elem: jquery选择器,用来获取要被固定的元素* @param opts:* - target: jquery选择器,用来获取表示固定范围的元素* - type: top|bottom,表示要固定的位置* - height: 要固定的元素的高度,由于高度在做页面时就是确定的并且几乎不会被DOM操作改变,直接从外部传入可以除去获取元素高度的操作* - wait: 滚动事件回调的节流时间,控制回调至少隔多长时间才执行一次* - getStickyWidth:获取要固定元素的宽度,window resize或者DOM操作会导致固定元素的宽度发生变化,需要这个回调来刷新stickyWidth*/var Sticky = function (elem, opts) {var $elem = $(elem), $target = $(opts.target || $elem.data("target"));if (!$elem.length || !$target.length) return;var stickyWidth, $win = $(window),stickyHeight = opts.height || $elem[0].offsetHeight,rules = {top: function (rect) {return rect.top < 0 && (rect.bottom - stickyHeight) > 0;},bottom: function (rect) {var docClientWidth = document.documentElement.clientHeight;return rect.bottom > docClientWidth && (rect.top + stickyHeight) < docClientWidth;}},type = (opts.type in rules) && opts.type || "top",className = "sticky--in-" + type;refreshStickyWidth();$win.scroll(throttle(sticky, $.isNumeric(opts.wait) && parseInt(opts.wait) || 100));$win.resize(throttle(function () {refreshStickyWidth();sticky();}, 50));function refreshStickyWidth() {stickyWidth = typeof opts.getStickyWidth === "function" && opts.getStickyWidth($elem) || $elem[0].offsetWidth;$elem.hasClass(className) && $elem.css("width", stickyWidth + "px");}//效果实现function sticky() {if (rules[type]($target[0].getBoundingClientRect())) {!$elem.hasClass(className) && $elem.addClass(className).css("width", stickyWidth + "px");} else {$elem.hasClass(className) && $elem.removeClass(className).css("width", "auto");}}//函数节流function throttle(func, wait) {var timer = null;return function () {var self = this, args = arguments;if (timer) clearTimeout(timer);timer = setTimeout(function () {return typeof func === "function" && func.apply(self, args);}, wait);}}};调用方式,固定在顶部的情况(type选项默认为top):<script>new Sticky("#sticky",{height: 52,getStickyWidth: function($elem){return ($elem.parent()[0].offsetWidth - 30);}});</script>固定在底部的情况:<script>new Sticky("#sticky",{height: 52,type: "bottom",getStickyWidth: function($elem){return ($elem.parent()[0].offsetWidth - 30);}});</script>还有一个要说明的是,opts的getStickyWidth选项,这个回调用来获取sticky元素的宽度,为什么要把它放出来,通过外部去获取宽度,而不是在组件内部通过offsetWidth获取?是因为当sticky元素的外部容器是自适应的时候,sticky元素固定时的宽度不是由sticky元素自己决定的,而是依赖于外部容器的宽度,所以这个宽度只能在外部去获取,内部获取不准确。比如上面的代码中我减了一个30,如果在组件内部获取的话,我肯定不知道要添加减30这样一个逻辑。