淺談Sticky組件的改進(jìn)實(shí)現(xiàn)
在上一篇文章使用getBoundingClientRect方法實(shí)現(xiàn)簡潔的sticky組件的方法介紹了一個(gè)sticky組件的簡潔實(shí)現(xiàn),經(jīng)過這兩天的思考,發(fā)現(xiàn)上次提供的實(shí)現(xiàn)還有較多不足的地方,另外跟別的網(wǎng)站上實(shí)現(xiàn)的效果在取消固定的時(shí)候也有一些不同,上次提供的取消固定的處理方式不好,本文在上文的基礎(chǔ)上,提供一個(gè)改進(jìn)版的sticky組件,功能更加完善,希望您有興趣閱讀。
1. 舊版本的問題
上一個(gè)sticky組件的實(shí)現(xiàn)中,有多個(gè)問題存在:
第一,從sticky的效果上來說,sticky元素在固定前后,不會(huì)變化的是相對瀏覽器左邊的位置以及sticky元素的整體寬度,可能會(huì)變化的是相對瀏覽器頂部或底部的位置和sticky元素的高度,而上文提供的實(shí)現(xiàn)中把后面兩個(gè)會(huì)變化的值都當(dāng)成了不變的值。為什么在固定的時(shí)候top值或bottom值就一定是0?當(dāng)然可以不是0阿,比如top: 20px,bottom: 15px,在某些場景里,加上一些這樣的偏移,sticky的效果會(huì)更好看,比如bootstrap官方文檔中用到的affix組件實(shí)例(這個(gè)組件的功能跟本文實(shí)現(xiàn)的sticky組件是差不多的):
它就把固定的時(shí)候,相對瀏覽器頂部的位置設(shè)置成了top: 20px。sticky元素的高度也是,為了在固定的時(shí)候顯示更好看的效果,調(diào)整原來的Line-height或者padding-top等更高度有關(guān)的屬性,也是非常常見的需求,比如天貓花唄的這個(gè)頁面,這塊內(nèi)容就用到了sticky組件:
固定前,sticky元素的高度是:
固定后,sticky元素的高度是:
第二,在取消固定的時(shí)候,以sticky元素固定在頂部為例,上文提供的實(shí)現(xiàn)是在target元素跟瀏覽器頂部的距離小于stickyHeight的時(shí)候,就直接取消sticky元素的position: fixed屬性,sticky元素立馬被還原到普通文檔流中,效果是:
它是在臨界點(diǎn)的時(shí)候立馬就消失的,而天貓花唄的那個(gè)效果就不是這樣:
它在臨界點(diǎn)的時(shí)候并不是立即消失,而是重新去調(diào)整sticky元素的top值,讓它配合著滾動(dòng)條一起跟隨網(wǎng)頁主體內(nèi)容一起向上滾動(dòng):
從體驗(yàn)上來說,顯然天貓花唄的這個(gè)效果更好一點(diǎn),從功能上來說,上文提供的實(shí)現(xiàn)有一個(gè)致命的缺點(diǎn):就是當(dāng)sticky元素的高度非常大,超出了瀏覽器可視區(qū)域的高度的時(shí)候,會(huì)出現(xiàn)不管你怎么滾動(dòng),都無法瀏覽全sticky元素所有內(nèi)容的BUG,有興趣的可以拿上次實(shí)現(xiàn)的代碼在自己博客的側(cè)邊欄上試一試。我試過發(fā)現(xiàn)了這個(gè)問題,所以才想要改進(jìn)sticky組件:(
第三,上次的實(shí)現(xiàn)還有幾處不足的地方:
1)documentElement.clientHeight沒有做緩存,導(dǎo)致每次判斷臨界點(diǎn)時(shí)都要去重新獲取:
2)滾動(dòng)回調(diào)間隔的默認(rèn)值太大,應(yīng)該再設(shè)置小一點(diǎn),這次用的是5,bootstrap用的是1,只有這樣才能保證效果流暢;
3)有的場景可能不需要resize的時(shí)候重新設(shè)置sticky元素的寬度,應(yīng)該加個(gè)選項(xiàng)來控制;
4)在sticky元素固定和取消固定的時(shí)候,應(yīng)該提供回調(diào)函數(shù),以便其它組件依賴這個(gè)組件的時(shí)候可以在關(guān)鍵點(diǎn)做些事情。
2. 如何改進(jìn)
組件的選項(xiàng)重新定義了一下:
var DEFAULTS = { target: '', //target元素的jq選擇器 type: 'top', //固定的位置,top | bottom,默認(rèn)為top,表示固定在頂部 wait: 5, //scroll事件回調(diào)的間隔 stickyOffset: 0, //固定時(shí)距離瀏覽器可視區(qū)頂部或底部的偏移,用來設(shè)置top跟bottom屬性的值,默認(rèn)為0 isFixedWidth: true, //sticky元素寬度是否固定,默認(rèn)為true,如果是自適應(yīng)的寬度,需設(shè)置為false getStickyWidth: undefined, //用來獲取sticky元素寬度的回調(diào),在不傳該參數(shù)的情況下,stickyWidth將設(shè)置為sticky元素的offsetWidth unStickyDistance: undefined, //該參數(shù)決定sticky元素何時(shí)進(jìn)入dynamicSticky狀態(tài) onSticky: undefined, ///sticky元素固定時(shí)的回調(diào) onUnSticky: undefined ///sticky元素取消固定時(shí)的回調(diào) };
加粗的幾個(gè)是新增或有修改的,去掉了原來的height,用unStickyDistance來替代。固定時(shí)候相對瀏覽器頂部或底部的位置,用stickyOffset來指定,這樣在.sticky--in-top或.sticky--in-bottom的css里就不用再寫top或bottom屬性值了。isFixedWidth如果為false,才會(huì)去添加resize時(shí)刷新sticky元素寬度的回調(diào):
!opts.isFixedWidth && $win.resize(throttle(function () { setStickyWidth(); $elem.hasClass(className) && $elem.css('width', stickyWidth); sticky(); }, opts.wait));
本次實(shí)現(xiàn)相比上次,麻煩的是取消固定時(shí)的邏輯處理,上次sticky元素只有2種狀態(tài),sticky或者unsticky,這次不一樣,sticky狀態(tài)里面又分成了staticSticky和dynamicSticky,前者表示top或bottom值不變的sticky狀態(tài),后者表示top或bottom值會(huì)變化的sticky狀態(tài),其實(shí)后者對應(yīng)的就是快要取消固定的時(shí)候那段范圍,為了更清晰地解決這個(gè)問題,將原來判斷臨界點(diǎn)以及在不同臨界點(diǎn)做不同處理的代碼重構(gòu)成下面這個(gè)樣子:
setSticky = function () { !$elem.hasClass(className) && $elem.addClass(className).css('width', stickyWidth) && (typeof opts.onSticky == 'function' && opts.onSticky($elem, $target)); return true; }, states = { staticSticky: function () { setSticky() && $elem.css(opts.type, opts.stickyOffset); }, dynamicSticky: function (rect) { setSticky() && $elem.css(opts.type, rules[opts.type].getDynamicOffset(rect)); }, unSticky: function () { $elem.hasClass(className) && $elem.removeClass(className).css('width', '').css(opts.type, '') && (typeof opts.onUnSticky == 'function' && opts.onUnSticky($elem, $target)); } }, rules = { top: { getState: function (rect) { if (rect.top < 0 && (rect.bottom - unStickyDistance) > 0) return 'staticSticky'; else if ((rect.bottom - unStickyDistance) <= 0 && rect.bottom > 0) return 'dynamicSticky'; else return 'unSticky'; }, getDynamicOffset: function (rect) { return -(unStickyDistance - rect.bottom); } }, bottom: { getState: function (rect) { if (rect.bottom > docClientHeight && (rect.top + unStickyDistance) < docClientHeight) return 'staticSticky'; else if ((rect.top + unStickyDistance) >= docClientHeight && rect.top < docClientHeight) return 'dynamicSticky'; else return 'unSticky'; }, getDynamicOffset: function (rect) { return -(unStickyDistance + rect.top - docClientHeight); } } } $win.scroll(throttle(sticky, opts.wait)); function sticky() { var rect = $target[0].getBoundingClientRect(), curState = rules[opts.type].getState(rect); states[curState](rect); }
有點(diǎn)狀態(tài)模式的思想在里面,不過更簡潔。當(dāng)我寫出這個(gè)代碼的時(shí)候,其實(shí)是很想用之前了解的狀態(tài)機(jī)來寫的,我想過用狀態(tài)機(jī)來寫肯定是可以實(shí)現(xiàn)的,不過為了少引用一個(gè)類庫就算了,等哪天想實(shí)踐狀態(tài)機(jī)的時(shí)候再來嘗試一把。
整體實(shí)現(xiàn)如下:
var Sticky = (function ($) { 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); } } var DEFAULTS = { target: '', //target元素的jq選擇器 type: 'top', //固定的位置,top | bottom,默認(rèn)為top,表示固定在頂部 wait: 5, //scroll事件回調(diào)的間隔 stickyOffset: 0, //固定時(shí)距離瀏覽器可視區(qū)頂部或底部的偏移,用來設(shè)置top跟bottom屬性的值,默認(rèn)為0 isFixedWidth: true, //sticky元素寬度是否固定,默認(rèn)為true,如果是自適應(yīng)的寬度,需設(shè)置為false getStickyWidth: undefined, //用來獲取sticky元素寬度的回調(diào),在不傳該參數(shù)的情況下,stickyWidth將設(shè)置為sticky元素的offsetWidth unStickyDistance: undefined, //該參數(shù)決定sticky元素何時(shí)進(jìn)入dynamicSticky狀態(tài) onSticky: undefined, ///sticky元素固定時(shí)的回調(diào) onUnSticky: undefined ///sticky元素取消固定時(shí)的回調(diào) }; return function (elem, opts) { var $elem = $(elem); opts = $.extend({}, DEFAULTS, opts || {}, $elem.data() || {}); var $target = $(opts.target); if (!$elem.length || !$target.length) return; var stickyWidth, setStickyWidth = function () { stickyWidth = typeof opts.getStickyWidth === 'function' && opts.getStickyWidth($elem) || $elem[0].offsetWidth; }, docClientHeight = document.documentElement.clientHeight, unStickyDistance = opts.unStickyDistance || $elem[0].offsetHeight, setSticky = function () { !$elem.hasClass(className) && $elem.addClass(className).css('width', stickyWidth) && (typeof opts.onSticky == 'function' && opts.onSticky($elem, $target)); return true; }, states = { staticSticky: function () { setSticky() && $elem.css(opts.type, opts.stickyOffset); }, dynamicSticky: function (rect) { setSticky() && $elem.css(opts.type, rules[opts.type].getDynamicOffset(rect)); }, unSticky: function () { $elem.hasClass(className) && $elem.removeClass(className).css('width', '').css(opts.type, '') && (typeof opts.onUnSticky == 'function' && opts.onUnSticky($elem, $target)); } }, rules = { top: { getState: function (rect) { if (rect.top < 0 && (rect.bottom - unStickyDistance) > 0) return 'staticSticky'; else if ((rect.bottom - unStickyDistance) <= 0 && rect.bottom > 0) return 'dynamicSticky'; else return 'unSticky'; }, getDynamicOffset: function (rect) { return -(unStickyDistance - rect.bottom); } }, bottom: { getState: function (rect) { if (rect.bottom > docClientHeight && (rect.top + unStickyDistance) < docClientHeight) return 'staticSticky'; else if ((rect.top + unStickyDistance) >= docClientHeight && rect.top < docClientHeight) return 'dynamicSticky'; else return 'unSticky'; }, getDynamicOffset: function (rect) { return -(unStickyDistance + rect.top - docClientHeight); } } }, className = 'sticky--in-' + opts.type, $win = $(window); setStickyWidth(); $win.scroll(throttle(sticky, opts.wait)); !opts.isFixedWidth && $win.resize(throttle(function () { setStickyWidth(); $elem.hasClass(className) && $elem.css('width', stickyWidth); sticky(); }, opts.wait)); $win.resize(throttle(function () { docClientHeight = document.documentElement.clientHeight; }, opts.wait)); function sticky() { var rect = $target[0].getBoundingClientRect(), curState = rules[opts.type].getState(rect); states[curState](rect); } } })(jQuery);
難理解的可能是getState的那個(gè)方法的邏輯,這部分的一些思路在上上篇博客有比較詳細(xì)的說明。
3. 博客側(cè)邊欄應(yīng)用說明
首先得把本次的實(shí)現(xiàn)粘貼到博客設(shè)置頁腳html文本域里面去,然后加入下面的代碼來初始化:
var timer = setInterval(function(){ if($('#blogCalendar').length && $('#profile_block').length && $('#sidebar_search').length) { new Sticky('#sideBar', { target: '#main', onSticky: function($elem, $target){ $target.css('min-height',$elem.outerHeight()); $elem.css('left', '65px'); }, onUnSticky: function($elem, $target){ $target.css('min-height',''); $elem.css('left', ''); } }); } },100);
使用timer是因?yàn)閭?cè)邊欄的內(nèi)容都是ajax加載,又不可能在這些ajax請求時(shí)候添加回調(diào),只能通過它們返回的內(nèi)容來判斷側(cè)邊欄是否加載完畢。
4. 總結(jié)
這周末琢磨了下如何改進(jìn)sticky組件,加上寫這篇文章,花了大半天的時(shí)間,好歹現(xiàn)在這個(gè)sticky組件的功能跟實(shí)現(xiàn)能讓自己有點(diǎn)滿意的感覺了,上次寫完總覺得怪怪的,好像缺點(diǎn)什么,原來是因?yàn)檫€差這么多東西。現(xiàn)在這個(gè)組件還只是能實(shí)現(xiàn)固定和取消固定的效果,對于實(shí)際工作而言,這個(gè)層級(jí)的效果可能還不夠,網(wǎng)上常見的那種在固定的同時(shí)支持導(dǎo)航滾動(dòng)或者tab導(dǎo)航的功能也很常見,下篇文章會(huì)介紹基于本文的sticky組件,如何實(shí)現(xiàn)navScrollSticky以及tabSticky組件,敬請關(guān)注。
感謝您的閱讀:)
補(bǔ)充說明:
IE跟火狐里面,在刷新頁面的時(shí)候,如果刷新前頁面有滾動(dòng),刷新的操作雖然還會(huì)把頁面的滾動(dòng)位置設(shè)置成刷新的位置,但是不會(huì)觸發(fā)scroll事件,所以必須在組件初始化之后立即調(diào)用一次sticky函數(shù):
- 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)航的方法
- 使用getBoundingClientRect方法實(shí)現(xiàn)簡潔的sticky組件的方法
相關(guān)文章
Javascript的console['''']常用輸入方法匯總
本文給大家?guī)砹耸畮追NJavascript的console['']常用輸入方法,每種方法給大家介紹的都很詳細(xì),需要的朋友參考下吧2018-04-04JS+CSS實(shí)現(xiàn)的簡單折疊展開多級(jí)菜單效果
這篇文章主要介紹了JS+CSS實(shí)現(xiàn)的簡單折疊展開多級(jí)菜單效果,涉及JavaScript頁面元素的遍歷及動(dòng)態(tài)操作技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-09-09js獲取對象,數(shù)組所有屬性鍵值(key)和對應(yīng)值(value)的方法示例
這篇文章主要介紹了js獲取對象,數(shù)組所有屬性鍵值(key)和對應(yīng)值(value)的方法,涉及javascript對于對象、數(shù)組鍵名與鍵值遍歷相關(guān)操作技巧,需要的朋友可以參考下2019-06-06js使用文件流下載csv文件的實(shí)現(xiàn)方法
這篇文章主要給大家介紹了關(guān)于js使用文件流下載csv文件的實(shí)現(xiàn)方法,文中通過示例代碼介紹的非常詳細(xì),對大家學(xué)習(xí)或者使用js具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧2019-07-07Javascript驗(yàn)證上傳圖片大小[前臺(tái)處理]
在做上傳圖片的時(shí)候,如果不限制上傳圖片大小,后果非常的嚴(yán)重。解決這個(gè)問題有兩種方式:后臺(tái)處理、前臺(tái)處理2014-07-07JS實(shí)現(xiàn)的簡單輪播圖運(yùn)動(dòng)效果示例
這篇文章主要介紹了JS實(shí)現(xiàn)的簡單輪播圖運(yùn)動(dòng)效果,結(jié)合完整實(shí)例形式分析了javascript基于定時(shí)器動(dòng)態(tài)修改頁面元素屬性的相關(guān)操作技巧,需要的朋友可以參考下2016-12-12