這篇文章是三天前寫就的,有大佬給我提了一些修改意見,我以爲這個意見確實中肯。因此就有了這個升級的修改版本。代碼同步更新到 GitHub 了。html
修改內容以下:前端
添加了圖文說明,直觀的說明 getBoundingClientRect()集合含義vue
頻繁 reflow 風險該如何規避(優化滾動監聽)ios
修改更新的內容在第 4 點和第 5 點,若是你看過本文,能夠直接看修改更新的內容。或者你能夠再看一遍。web
我入職第二家公司接到的第一個需求就是修復以前外包作的滾動吸頂效果。我當時很納悶爲什麼一個滾動吸頂會有 bug,後來我查看代碼才發現直接用的是 offsetTop 這個屬性,並且並無作兼容性處理。瀏覽器
用於得到當前元素到定位父級( element.offsetParent )頂部的距離(偏移值)。性能優化
定位父級 offsetParent 的定義是:與當前元素最近的 position != static 的父級元素。微信
或許寫這個代碼的人沒有注意到「定位父級」這個這個附屬條件。網絡
後來在項目中總會遇到滾動吸頂的效果須要實現,如今我將我知道的 4 種滾動吸頂實現方式作詳細介紹。異步
使用 position:sticky 實現
使用 JQuery 的 offset().top 實現
使用原生的 offsetTop 實現
使用 obj.getBoundingClientRect().top 實現
以上這四種方式你都瞭解嗎?相關代碼已上傳到 GitHub ,感興趣的能夠 clone 代碼到本地運行。望給個 star 支持一下。
咱們先看一下效果圖:
1、使用 position:sticky 實現
粘性定位 sticky 至關於相對定位 relative 和固定定位 fixed 的結合;在頁面元素滾動過程當中,某個元素距離其父元素的距離達到 sticky 粘性定位的要求時;元素的相對定位 relative 效果變成固定定位 fixed 的效果。
MDN 傳送門
使用條件:
父元素不能 overflow:hidden 或者 overflow:auto 屬性
必須指定 top、bottom、left、right 4 個值之一,不然只會處於相對定位
父元素的高度不能低於 sticky 元素的高度
在須要滾動吸頂的元素加上如下樣式即可以實現這個效果:
.sticky { position: -webkit-sticky; position: sticky; top: 0; }
咱們先看下在 Can I use 中看看這個屬性的兼容性:
能夠看出這個屬性的兼容性並非很好,由於這個 API 還只是實驗性的屬性。不過這個 API 在 IOS 系統的兼容性仍是比較好的。
因此咱們在生產環境若是使用這個 API 的時候通常會和下面的幾種方式結合使用。
2、使用 JQuery 的 offset().top 實現
咱們知道 JQuery 中封裝了操做 DOM 和讀取 DOM 計算屬性的 API,基於 offset().top 這個 API 和 scrollTop() 的結合,咱們也能夠實現滾動吸頂效果。
... window.addEventListener('scroll', self.handleScrollOne); ... handleScrollOne: function() { let self = this; let scrollTop = $('html').scrollTop(); let offsetTop = $('.title_box').offset().top; self.titleFixed = scrollTop > offsetTop; } ...
這樣實現當然能夠,不過因爲 JQuery 慢慢的退出歷史的舞臺,咱們在代碼中儘可能不使用 JQuery 的 API。咱們能夠基於 offset().top 的源碼本身處理原生 offsetTop。因而乎就有了第三種方式。
scrolloTop() 有兼容性問題,在微信瀏覽器、IE、某些 firefox 版本中 $('html').scrollTop() 的值會爲 0,因而乎也就有了第三種方案的兼容性寫法。
3、使用原生的 offsetTop 實現
咱們知道 offsetTop 是相對定位父級的偏移量,假若須要滾動吸頂的元素出現定位父級元素,那麼 offsetTop 獲取的就不是元素距離頁面頂部的距離。
咱們能夠本身對 offsetTop 作如下處理:
getOffset: function(obj,direction){ let offsetL = 0; let offsetT = 0; while( obj!== window.document.body && obj !== null ){ offsetL += obj.offsetLeft; offsetT += obj.offsetTop; obj = obj.offsetParent; } if(direction === 'left'){ return offsetL; }else { return offsetT; } }
使用:
... window.addEventListener('scroll', self.handleScrollTwo); ... handleScrollTwo: function() { let self = this; let scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop; let offsetTop = self.getOffset(self.$refs.pride_tab_fixed); self.titleFixed = scrollTop > offsetTop; } ...
你是否是看出了以上兩種方式的一些問題?
咱們必定須要使用 scrollTop-offsetTop 的值來實現滾動吸頂的效果嗎?答案是否認的。
咱們一同看看第四種方案。
4、使用 obj.getBoundingClientRect().top 實現
這個 API 能夠告訴你頁面中某個元素相對瀏覽器視窗上下左右的距離。
tab 吸頂可使用 obj.getBoundingClientRect().top 代替 scrollTop-offsetTop,代碼以下:
// html <div class="pride_tab_fixed" ref="pride_tab_fixed"> <div class="pride_tab" :class="titleFixed == true ? 'isFixed' :''"> // some code </div> </div> // vue export default { data(){ return{ titleFixed: false } }, activated(){ this.titleFixed = false; window.addEventListener('scroll', this.handleScroll); }, methods: { //滾動監聽,頭部固定 handleScroll: function () { let offsetTop = this.$refs.pride_tab_fixed.getBoundingClientRect().top; this.titleFixed = offsetTop < 0; // some code } } }
用於得到頁面中某個元素的左,上,右和下分別相對瀏覽器視窗的位置。不包含文檔卷起來的部分。
該函數返回一個 object 對象,有8個屬性: top,right,buttom,left,width,height,x,y
用於得到當前元素到定位父級( element.offsetParent )頂部的距離(偏移值)。
定位父級 offsetParent 的定義是:與當前元素最近的 position != static 的父級元素。
offsetTop 和 offsetParent 方法相結合能夠得到該元素到 body 上邊距的距離。代碼以下:
getOffset: function(obj,direction){ let offsetL = 0; let offsetT = 0; while( obj!== window.document.body && obj !== null ){ offsetL += obj.offsetLeft; offsetT += obj.offsetTop; obj = obj.offsetParent; } if(direction === 'left'){ return offsetL; }else { return offsetT; } }
元素在水平方向上佔用的空間大小:
offsetWidth = border-left + padding-left + width + padding-right + border-right
元素在垂直方向上佔用的空間大小:
offsetHeight = border-top + padding-top + height + padding-bottom + border-bottom
注:若是存在垂直滾動條,offsetWidth 也包括垂直滾動條的寬度;若是存在水平滾動條,offsetHeight 也包括水平滾動條的高度;
元素的上外邊框至 offsetParent 元素的上內邊框之間的像素距離;
元素的左外邊框至 offsetParent 元素的左內邊框之間的像素距離;
全部偏移量屬性都是隻讀的;
若是給元素設置了 display:none,則它的偏移量屬性都爲 0;
每次訪問偏移量屬性都須要從新計算(保存變量);
出現抖動的緣由是由於:在吸頂元素 position 變爲 fixed 的時候,該元素就脫離了文檔流,下一個元素就進行了補位。就是這個補位操做形成了抖動。
爲這個吸頂元素添加一個等高的父元素,咱們監聽這個父元素的 getBoundingClientRect().top 值來實現吸頂效果,即:
<div class="title_box" ref="pride_tab_fixed"> <div class="title" :class="titleFixed == true ? 'isFixed' :''"> 使用 `obj.getBoundingClientRect().top` 實現 </div> </div>
這個方案就能夠解決抖動的 Bug 了。
這個問題是我比較頭痛,以前我沒有在乎過這個問題。直到有一天我用美團點外賣的時候,我纔開始注意這個問題。
當頁面往下滾動時,吸頂元素須要等頁面滾動中止以後纔會出現吸頂效果
緣由:在 ios 系統上不能實時監聽 scroll 滾動監聽事件,在滾動中止時才觸發其相關的事件。
還記得第一種方案中的 position:sticky 嗎?這個屬性在 IOS6 以上的系統中有良好的兼容性,因此咱們能夠區分 IOS 和 Android 設備作兩種處理。
IOS 使用 position:sticky,Android 使用滾動監聽 getBoundingClientRect().top 的值。
若是 IOS 版本太低呢?這裏提供一種思路: window.requestAnimationFrame()。
性能優化篇(新增)
到此 4 中滾動吸頂的方式介紹完了,但是這樣就真的結束了嗎?其實仍是有優化的空間的。
咱們從兩個方向作性能優化(實際上是一個方向):
避免過分的 reflow
咱們知道過分的 reflow 會使頁面的性能降低。因此咱們須要儘量的下降 reflow 的次數,給用戶更加流暢的感受。
有的朋友或許會說這個我知道,但是這和滾動吸頂有什麼關係呢?
不急,你是否還記得滾動吸頂使用了 offsetTop 或者 getBoundingClientRect().top 來獲取響應的偏移量呢?
既然有讀取元素的屬性就天然會致使頁面 reflow。
所以咱們優化的方向就是從減小讀取元素屬性次數下手,查看代碼發現一觸發屏幕滾動事件就會調用相關方法讀取元素的偏移量。
解決這個問題有如下兩個方案:
犧牲平滑度知足性能,使用節流控制相關方法的調用
這個方案很常見,不過其帶來的反作用也很明顯,就是在吸頂效果會有些延遲,若是產品能夠接受那就不失爲一種好方法。
這樣能夠控制在必定時間內只讀取
這裏節流函數就直接是用 lodash.js 封裝好的 throttle 方法。
代碼以下:
window.addEventListener('scroll', _.throttle(self.handleScrollThree, 50));
第二種方案相對來講容易接受一點,就是支持 IntersectionObserver 就用 IntersectionObserver,不然就用 throttle。
咱們先講講 IntersectionObserver
IntersectionObserver 能夠用來監聽元素是否進入了設備的可視區域以內,而不須要頻繁的計算來作這個判斷。
經過這個屬性咱們就能夠在元素不在可視範圍內,不去讀取元素的相對位置,已達到性能優化;當瀏覽器不支持這個屬性的時候就使用 throttle 來處理。
咱們看看這個屬性的兼容性怎麼樣:
大概支持了 60% 以上,在項目中仍是可使用的(你須要作好兼容性處理)。
關於 IntersectionObserver 如何使用,請看 MDN 或者 阮一峯教程。
使用 IntersectionObserver 和 throttle 優化的代碼以下:
IntersectionObserverFun: function() { let self = this; let ele = self.$refs.pride_tab_fixed; if( !IntersectionObserver ){ let observer = new IntersectionObserver(function(){ let offsetTop = ele.getBoundingClientRect().top; self.titleFixed = offsetTop < 0; }, { threshold: [1] }); observer.observe(document.getElementsByClassName('title_box')[0]); } else { window.addEventListener('scroll', _.throttle(function(){ let offsetTop = ele.getBoundingClientRect().top; self.titleFixed = offsetTop < 0; }, 50)); } },
IntersectionObserver API 是異步的,不隨着目標元素的滾動同步觸發。
規格寫明,IntersectionObserver的實現,應該採用 requestIdleCallback()。它不會當即執行回調,它會調用 window.requestIdleCallback() 來異步的執行咱們指定的回調函數,並且還規定了最大的延遲時間是 100 毫秒。
總結:
這種 IntersectionObserver 和 throttle 結合的方案不失爲一種可選擇的方案,這種方案的優勢就在於能夠有效地減小頁面 reflow 的風險,不過缺點也是有的,須要犧牲頁面的平滑度。具體該如何取捨,就看業務的須要啦。
《前端詞典》這個系列會持續更新,每一期我都會講一個出現頻率較高的知識點。但願你們在閱讀的過程中能夠斧正文中出現不嚴謹或是錯誤的地方,本人將不勝感激;若經過本系列而有所得,本人亦將不勝欣喜。
內容: 前端以及網絡相關知識點的介紹並加以實際應用做爲輔助。
目的: 這個系列的文章能夠對讀者起到一點幫助,解開一些迷惑。
但願各位多指點一二,不吝賜教。
若是你以爲個人文章寫的還不錯,那就關注我唄。