使用getBoundingClientRect方法實(shí)現(xiàn)簡潔的sticky組件的方法
sticky組件,通常應(yīng)用于導(dǎo)航條或者工具欄,當(dāng)網(wǎng)頁在某一區(qū)域滾動(dòng)的時(shí)候,將導(dǎo)航條或工具欄這類元素固定在頁面頂部或底部,方便用戶快速進(jìn)行這類元素提供的操作。本文介紹這種組件的實(shí)現(xiàn)思路,并提供一個(gè)同時(shí)支持將sticky元素固定在頂部或底部的具體實(shí)現(xiàn),由于這種組件在網(wǎng)站中非常常見,所以有必要掌握它的實(shí)現(xiàn)方式,以便在有需要的時(shí)候基于它的思路寫出功能更多的組件出來。
固定在頂部的demo效果(對應(yīng)sticky-top.html):
固定在底部的demo效果(對應(yīng)sticky-bottom.html):
1. 實(shí)現(xiàn)思路
實(shí)現(xiàn)這個(gè)組件的關(guān)鍵在于找到元素何時(shí)被固定以及何時(shí)被取消固定的臨界點(diǎn),要找到這個(gè)臨界點(diǎn),首先要詳細(xì)看看前面demo的變化過程。在前面的demo中,有一個(gè)導(dǎo)航條元素,也就是我們要控制固定與否的元素,我把它稱為sticky元素;還有一個(gè)元素,它用來顯示網(wǎng)頁的一塊列表內(nèi)容,這個(gè)列表元素跟sticky元素在功能上是相關(guān)的,因?yàn)閟ticky元素要導(dǎo)航的正是這個(gè)列表元素提供的內(nèi)容,本文在開始介紹sticky組件的功能時(shí),就說過sticky組件固定是發(fā)生在網(wǎng)頁滾動(dòng)至某一區(qū)域的時(shí)候,離開這一區(qū)域就會取消固定,這個(gè)滾動(dòng)區(qū)域或者說滾動(dòng)范圍,就是由列表元素來決定的,所以這個(gè)列表元素是找到臨界點(diǎn)的關(guān)鍵,它表示sticky組件可被固定的網(wǎng)頁滾動(dòng)范圍,為了后面引用方便,我把這個(gè)元素稱為target元素。下面就來詳細(xì)了解下前面demo的變化過程,由于固定在底部的情況與固定在頂部的情況實(shí)現(xiàn)思路是相通的,如果弄明白了固定在頂部的實(shí)現(xiàn)原理,相信你也一定能弄明白固定在底部的實(shí)現(xiàn)原理,所以這里也是為了減少篇幅,提高效率,僅僅介紹固定在頂部的情況:
一開始sticky元素和target元素的狀態(tài)是這樣的:
當(dāng)滾動(dòng)條慢慢向下,使得網(wǎng)頁向上滾動(dòng)的時(shí)候,sticky元素和target元素在一段滾動(dòng)距離內(nèi)狀態(tài)并沒有發(fā)生變化,一直到這個(gè)狀態(tài)(滾動(dòng)條滾動(dòng)距離為573px):
在這個(gè)狀態(tài)只要滾動(dòng)條再往下滾動(dòng)1px,sticky元素就會被固定在頂部(滾動(dòng)條滾動(dòng)距離為574px):
也就是說當(dāng)target元素的頂部離瀏覽器頂部的距離小于0的時(shí)候(target元素的頂部未超出瀏覽器頂部的時(shí)候,距離看作大于0),sticky元素就會被固定,所以這就是我們要找的第一個(gè)臨界點(diǎn)。然后滾動(dòng)條繼續(xù)向下滾動(dòng),只要target元素還在瀏覽器可視區(qū)域內(nèi),sticky元素就會一直被固定:
直到這個(gè)狀態(tài)(滾動(dòng)條滾動(dòng)距離為1861px):
在這個(gè)狀態(tài)只要滾動(dòng)條再往下滾動(dòng)1px,sticky元素就會取消固定在頂部(滾動(dòng)條滾動(dòng)距離為1862px):
顯然,這就是我們要找的第2個(gè)臨界點(diǎn),不過它的判斷條件是:當(dāng)target元素的底部離瀏覽器頂部的距離小于sticky元素的高度時(shí),sticky元素就會被取消固定。這里為什么是小于sticky元素的高度,而不是小于0,原因是因?yàn)榛谛∮?這個(gè)臨界點(diǎn)開發(fā)出來的組件,會出現(xiàn)target元素幾乎快從瀏覽器可視區(qū)域消失了,但是sticky元素還固定在那的效果:
sticky還把footer的內(nèi)容給蓋住了,本來是為了方便用戶操作,結(jié)果影響了用戶操作,所以得把取消固定這個(gè)臨界點(diǎn)提前,而用sticky元素的高度最合適。
通過前面對demo變化過程的拆解,我們已經(jīng)得到了滾動(dòng)條一直向下滾動(dòng)時(shí),sticky狀態(tài)變化的兩個(gè)臨界點(diǎn):
1)當(dāng)target元素的頂部離瀏覽器頂部的距離小于0的時(shí)候,sticky元素就會被固定;
2)當(dāng)target元素的底部離瀏覽器頂部的距離小于sticky元素的高度時(shí),sticky元素就會被取消固定。
綜合這兩個(gè)臨界點(diǎn),可以得出滾動(dòng)條向下滾動(dòng)時(shí),sticky元素被固定的滾動(dòng)范圍的判斷條件是:target元素的頂部離瀏覽器頂部的距離小于0 并且 target元素的底部離瀏覽器頂部的距離大于sticky元素的高度。而且這個(gè)判斷條件,同樣適用于滾動(dòng)條向上滾動(dòng)的情況,因?yàn)闈L動(dòng)條一直向上滾動(dòng)時(shí),sticky狀態(tài)變化的臨界點(diǎn)是:
1)當(dāng)target元素的底部離瀏覽器頂部的距離大于sticky元素的高度時(shí),sticky元素就會被固定;
2)當(dāng)target元素的頂部離瀏覽器頂部的距離大于0的時(shí)候,sticky元素就會被取消固定。
(這兩個(gè)臨界點(diǎn),其實(shí)跟滾動(dòng)條向下滾動(dòng)時(shí)提到的兩個(gè)臨界點(diǎn),是一個(gè)意思,只不過是正話反著說而已)
所以只要得到【target元素的頂部離瀏覽器頂部的距離】,【target元素的底部離瀏覽器頂部的距離】,【sticky元素的高度】這三個(gè)值基本上就能實(shí)現(xiàn)這個(gè)組件了。這三個(gè)值中sticky元素的高度由設(shè)計(jì)圖決定,它從網(wǎng)頁一開始制作就是已知的,在定義組件的時(shí)候我們可以從外部傳進(jìn)去,雖然也能從js去獲取它的高度,不過顯然沒有必要增加額外的計(jì)算;另外兩個(gè)值【target元素的頂部離瀏覽器頂部的距離】,【target元素的底部離瀏覽器頂部的距離】,我們正好可以利用DOM提供的一個(gè)方法來獲取,這個(gè)方法是:getBoundingClientRect,這是一個(gè)兼容性很好的方法,它的調(diào)用方式是:
var target = document.getElementById('main-container'); var rect = target.getBoundingClientRect(); console.log(rect);
返回一個(gè)ClientRect對象,這個(gè)對象存儲元素框模型的一些信息,比如它的寬高度(width and height),以及元素框上下邊距離瀏覽器頂部邊緣的距離(top and bottom),左右邊距離瀏覽器左邊緣的距離(left and right):
top跟bottom恰恰就是我們要獲取的【target元素的頂部離瀏覽器頂部的距離】,【target元素的底部離瀏覽器頂部的距離】,而且當(dāng)框的頂部或底部未超出瀏覽器頂部的時(shí)候,top跟bottom都是大于0的值,而當(dāng)框的頂部或底部超出瀏覽器頂部的時(shí)候,top跟bottom是小于0的值:
當(dāng)我們找到了【target元素的頂部離瀏覽器頂部的距離】,【target元素的底部離瀏覽器頂部的距離】,【sticky元素的高度】這三個(gè)值,就可以用代碼來描述前面的判斷條件:
rect.top < 0 && (rect.bottom - stickyHeight) > 0;
(rect表示target元素調(diào)用getBoundingClientRect返回的對象,stickyHeight表示sticky元素的高度)
最后為了讓實(shí)現(xiàn)思路更加完整,雖然不詳細(xì)介紹固定在底部的情況的變化過程,我還是把這種情況的臨界點(diǎn)跟判斷方式補(bǔ)充進(jìn)來,它的臨界點(diǎn)是(這里列的是滾動(dòng)條向下滾動(dòng)時(shí)的臨界點(diǎn)):
1)當(dāng)target元素的頂部離瀏覽器頂部的距離 + sticky元素的高度 小于瀏覽器可視區(qū)域的高度時(shí),sticky元素被固定;
2)當(dāng)target元素的底部離瀏覽器的頂部的距離小于瀏覽器可視區(qū)域的高度時(shí),sticky元素被取消固定。
瀏覽器可視區(qū)域的高度,可用document.documentElement.clientHeight來獲取,這個(gè)屬性也是沒有兼容性問題的,判斷代碼為:
var docClientWidth = document.documentElement.clientHeight; rect.bottom > docClientWidth && (rect.top + stickyHeight) < docClientWidth;
2. 實(shí)現(xiàn)細(xì)節(jié)
1)html結(jié)構(gòu)
固定在頂部的html結(jié)構(gòu):
<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結(jié)構(gòu):
<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元素,還需要注意兩點(diǎn):
a. 順序問題,兩種結(jié)構(gòu)中,target元素與sticky的父元素順序位置是反的;
b. sticky元素外面必須包裹一層元素,而且還得給這一層元素設(shè)置height屬性:
.sticky-wrapper { margin-bottom: 10px; height: 52px; }
這是因?yàn)楫?dāng)sticky元素被固定的時(shí)候,它會脫離普通文檔流,所以要利用它的父元素把sticky元素的高度在普通文檔流中撐起來,以免在固定效果出現(xiàn)的時(shí)候,target元素的內(nèi)容出現(xiàn)跳動(dòng)的情況。
2)固定效果
讓一個(gè)元素固定在瀏覽器的某個(gè)位置,當(dāng)然是通過position: fixed來弄,所以可以用兩個(gè)css類來實(shí)現(xiàn)固定在頂部和固定在底部的效果:
.sticky--in-top,.sticky--in-bottom { position: fixed; z-index: 1000; } .sticky--in-top { top: 0; } .sticky--in-bottom { bottom: 0; }
當(dāng)我們判斷元素需要被固定在頂部的時(shí)候,就給它添加.sticky--in-top的css類;當(dāng)我們判斷元素需要被固定在底部的時(shí)候,就給它添加.sticky--in-bottom的css類。
3)滾動(dòng)回調(diào)
控制sticy元素固定的邏輯顯然要寫在window的scroll事件回調(diào)中(有了前面對實(shí)現(xiàn)思路以及判斷條件的說明,相信理解下面這段代碼應(yīng)該會很容易):
固定在頂部的回調(diào)邏輯:
$(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'); } });
其中:$target是target元素的jq對象,$elem是sticky元素的jq對象,stickyHeight是sticky元素的高度,stickyWidth是sticky元素的寬度。由于sticky元素固定時(shí),脫離原來的文檔流,需要設(shè)置寬度才能顯示跟固定前一樣的寬度。
固定在底部的回調(diào)邏輯:
$(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'); } });
這里是為了把回調(diào)邏輯說的更清楚才把代碼分成兩份,最后給的實(shí)現(xiàn)會把這兩個(gè)代碼合并成一份:)
4)函數(shù)節(jié)流
函數(shù)節(jié)流通常應(yīng)用于window的scroll事件,resize事件以及普通元素的mousemove事件,因?yàn)檫@些事件由于鼠標(biāo)或滾輪操作很頻繁,會導(dǎo)致回調(diào)連續(xù)觸發(fā),如果回調(diào)里面含有DOM操作,這種連續(xù)調(diào)用就會影響頁面的性能,所以很有必要控制這類回調(diào)的執(zhí)行次數(shù),函數(shù)節(jié)流就是做這個(gè)的,我這里提供了一個(gè)很簡單的函數(shù)節(jié)流實(shí)現(xiàn):
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); } }
這個(gè)函數(shù)可以控制func所指定的函數(shù),執(zhí)行的間隔指定為wait指定的毫秒數(shù),利用它,我們可以把前面的滾動(dòng)回調(diào)改動(dòng)一下,比如固定在頂部的情況改成:
$(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);
其實(shí)真正處理回調(diào)的是throttle返回的函數(shù),這個(gè)返回的函數(shù)邏輯少,而且沒有DOM操作,它是會被連續(xù)調(diào)用的,但是不影響頁面性能,而我們真正處理邏輯的那個(gè)函數(shù),也就是傳入throttle的那個(gè)函數(shù)因?yàn)閠hrottle創(chuàng)建的閉包的作用,不會被連續(xù)調(diào)用,這樣就實(shí)現(xiàn)了控制函數(shù)執(zhí)行次數(shù)的目的。
5)resize的問題
window resize總是在定義組件的時(shí)候帶來問題,因?yàn)轫撁婵梢晠^(qū)域的寬高度發(fā)生了變化,sticky元素的父容器寬度也可能發(fā)生了變化,而且resize的時(shí)候不會觸發(fā)scroll事件,所以我們需要在resize回調(diào)內(nèi),刷新sticky元素的寬度以及重新調(diào)用固定效果的邏輯,這個(gè)相關(guān)的代碼就不貼出來了,后面直接看整體實(shí)現(xiàn)吧,否則我怕放出來會影響理解??傊畆esize是我們在定義組件的時(shí)候肯定要考慮的,不過一般都放到最后來處理,有點(diǎn)算處理BUG之類的工作。
3. 整體實(shí)現(xiàn)
代碼比較簡潔:
/** * @param elem: jquery選擇器,用來獲取要被固定的元素 * @param opts: * - target: jquery選擇器,用來獲取表示固定范圍的元素 * - type: top|bottom,表示要固定的位置 * - height: 要固定的元素的高度,由于高度在做頁面時(shí)就是確定的并且?guī)缀醪粫籇OM操作改變,直接從外部傳入可以除去獲取元素高度的操作 * - wait: 滾動(dòng)事件回調(diào)的節(jié)流時(shí)間,控制回調(diào)至少隔多長時(shí)間才執(zhí)行一次 * - getStickyWidth:獲取要固定元素的寬度,window resize或者DOM操作會導(dǎo)致固定元素的寬度發(fā)生變化,需要這個(gè)回調(diào)來刷新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'); } //效果實(shí)現(xiàn) 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'); } } //函數(shù)節(jié)流 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); } } };
調(diào)用方式,固定在頂部的情況(type選項(xiàng)默認(rèn)為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>
還有一個(gè)要說明的是,opts的getStickyWidth選項(xiàng),這個(gè)回調(diào)用來獲取sticky元素的寬度,為什么要把它放出來,通過外部去獲取寬度,而不是在組件內(nèi)部通過offsetWidth獲取?是因?yàn)楫?dāng)sticky元素的外部容器是自適應(yīng)的時(shí)候,sticky元素固定時(shí)的寬度不是由sticky元素自己決定的,而是依賴于外部容器的寬度,所以這個(gè)寬度只能在外部去獲取,內(nèi)部獲取不準(zhǔn)確。比如上面的代碼中我減了一個(gè)30,如果在組件內(nèi)部獲取的話,我肯定不知道要添加減30這樣一個(gè)邏輯。
4. 總結(jié)
本文提供了一個(gè)很常見的sticky組件實(shí)現(xiàn),實(shí)現(xiàn)這個(gè)組件的關(guān)鍵在于找到控制sticky元素固定與否的關(guān)鍵點(diǎn),同時(shí)在實(shí)現(xiàn)的時(shí)候函數(shù)節(jié)流跟window resize的問題需要特別注意。
我一直認(rèn)為對于一些簡單的組件,掌握它的思路,自己去定義比直接從github上去找開源的插件要來的更切實(shí)際:
1)代碼可控,不用去閱讀別人的代碼,有問題也能快速修改
2)代碼量小,開源的插件會盡可能多做事,而有些工作你的項(xiàng)目并不一定需要它去做;
3)更貼合項(xiàng)目的實(shí)際需求,跟第2點(diǎn)差不多的意思,在已有的思路基礎(chǔ)上,我們能開發(fā)出與項(xiàng)目需求完全契合的功能模塊;
4)有助于提高自己的技術(shù)水平,增進(jìn)知識的廣度和深度;
所以有能力造輪子的時(shí)候,造造也是很有必要的。
本文雖然在最后提供了整體的組件實(shí)現(xiàn),但是并不是建議拿來就用,否則前面大篇幅地去介紹實(shí)現(xiàn)思路就沒有必要了,我只要放個(gè)github地址即可,思路遠(yuǎn)比實(shí)現(xiàn)重要。我最近幾篇博客都是在分享思路,而不是分享某個(gè)具體的實(shí)現(xiàn),思路這種抽象的東西是通用的,理解前它不是你的,理解后它就存在于腦袋里,任何時(shí)候都可以拿來就用,我提供的思路也同樣來自于我對其它博客其它插件源碼學(xué)習(xí)之后的思考與總結(jié)。
補(bǔ)充于說明:
本文實(shí)現(xiàn)有不足,不完美的地方,請?jiān)诹私獗疚南嚓P(guān)內(nèi)容后,移步閱讀《sticky組件的改進(jìn)實(shí)現(xiàn)》了解更佳的實(shí)現(xiàn)。
- Firefox getBoxObjectFor getBoundingClientRect聯(lián)系
- 各種常用瀏覽器getBoundingClientRect的解析
- javascript getBoundingClientRect() 來獲取頁面元素的位置的代碼[修正版]
- javascript 獲取元素位置的快速方法 getBoundingClientRect()
- js getBoundingClientRect() 來獲取頁面元素的位置
- 獲取元素距離瀏覽器周邊的位置的方法getBoundingClientRect
- 使用Sticky組件實(shí)現(xiàn)帶sticky效果的tab導(dǎo)航和滾動(dòng)導(dǎo)航的方法
- 淺談Sticky組件的改進(jìn)實(shí)現(xiàn)
相關(guān)文章
javascript函數(shù)特點(diǎn)實(shí)例分析
這篇文章主要介紹了javascript函數(shù)特點(diǎn),實(shí)例分析了javascript函數(shù)傳遞參數(shù)及調(diào)用方法,需要的朋友可以參考下2015-05-05JavaScript實(shí)現(xiàn)頁面中錄音功能的方法
這篇文章主要給大家介紹了關(guān)于JavaScript實(shí)現(xiàn)頁面中錄音功能的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家學(xué)習(xí)或者使用JavaScript具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧2019-06-06javascript實(shí)現(xiàn)tabs選項(xiàng)卡切換效果(自寫原生js)
常用的頁面效果有彈出層效果,無縫滾動(dòng)效果,選項(xiàng)卡切換效果,接下來與大家分享一款自己用原生javascript寫的選項(xiàng)卡切換效果,感興趣的朋友可以參考下哈2013-03-03js根據(jù)給定的日期計(jì)算當(dāng)月有多少天實(shí)現(xiàn)思路及代碼
根據(jù)給定的日期計(jì)算當(dāng)月有多少天,想必這樣的功能大家都想實(shí)現(xiàn)吧,所以本文的出現(xiàn)相當(dāng)有必要,接下來看下實(shí)現(xiàn)代碼,感興趣的朋友可以了解下,希望對你有所幫助2013-02-02JavaScript實(shí)現(xiàn)二叉樹的先序、中序及后序遍歷方法詳解
這篇文章主要介紹了JavaScript實(shí)現(xiàn)二叉樹的先序、中序及后序遍歷方法,結(jié)合實(shí)例形式總結(jié)分析了javascript二叉樹的先序、中序及后序遍歷實(shí)現(xiàn)方法與相關(guān)操作注意事項(xiàng),需要的朋友可以參考下2017-10-10