【前端詞典】4 種滾動吸頂實現方式的比較

小生方勤發表於2019-04-09

前言

我入職第二家公司接到的第一個需求就是修復之前外包做的滾動吸頂效果。我當時很納悶為何一個滾動吸頂會有 bug,後來我檢視程式碼才發現直接用的是 offsetTop 這個屬性,而且並沒有做相容性處理。

offsetTop

用於獲得當前元素到定位父級( element.offsetParent )頂部的距離(偏移值)。

定位父級 offsetParent 的定義是:與當前元素最近的 position != static 的父級元素。

或許寫這個程式碼的人沒有注意到“定位父級”這個這個附屬條件。

後來在專案中總會遇到滾動吸頂的效果需要實現,現在我將我知道的 4 種滾動吸頂實現方式做詳細介紹。

目錄

  1. 使用 position:sticky 實現
  2. 使用 JQuery 的 offset().top 實現
  3. 使用原生的 offsetTop 實現
  4. 使用 obj.getBoundingClientRect().top 實現

以上這四種方式你都瞭解嗎?相關程式碼已上傳到 GitHub ,感興趣的可以 clone 程式碼到本地執行。望給個 star 支援一下。

四種實現方式

一、使用 position:sticky 實現

1、粘性定位是什麼?

粘性定位 sticky 相當於相對定位 relative 和固定定位 fixed 的結合;在頁面元素滾動過程中,某個元素距離其父元素的距離達到 sticky 粘性定位的要求時;元素的相對定位 relative 效果變成固定定位 fixed 的效果。

MDN 傳送門

2、如何使用?

使用條件:

  1. 父元素不能 overflow:hidden 或者 overflow:auto 屬性
  2. 必須指定 top、bottom、left、right 4 個值之一,否則只會處於相對定位
  3. 父元素的高度不能低於 sticky 元素的高度
  4. sticky 元素僅在其父元素內生效

在需要滾動吸頂的元素加上以下樣式便可以實現這個效果:

.sticky {
    position: -webkit-sticky;
    position: sticky;
    top: 0;
}
複製程式碼

3、這個屬性好用嗎?

我們先看下在 Can I use 中看看這個屬性的相容性:

【前端詞典】4 種滾動吸頂實現方式的比較

可以看出這個屬性的相容性並不是很好,因為這個 API 還只是實驗性的屬性。不過這個 API 在 IOS 系統的相容性還是比較好的。

所以我們在生產環境如果使用這個 API 的時候一般會和下面的幾種方式結合使用。

二、使用 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,於是乎也就有了第三種方案的相容性寫法。

三、使用原生的 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 的值來實現滾動吸頂的效果嗎?答案是否定的。

我們一同看看第四種方案。

四、使用 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
      }
    }
  }
複製程式碼

offsetTop 和 getBoundingClientRect() 區別

1. getBoundingClientRect():

用於獲得頁面中某個元素的左,上,右和下分別相對瀏覽器視窗的位置。不包含文件卷起來的部分。

該函式返回一個 object 物件,有6個屬性: top, right, buttom, left, width, height。 (在 IE 中,預設座標從(2,2)開始計算,只返回 top,lef,right,bottom 四個值)

2. offsetTop:

用於獲得當前元素到定位父級( element.offsetParent )頂部的距離(偏移值)。

定位父級 offsetParent 的定義是:與當前元素最近的 position != static 的父級元素。

offsetTopoffsetParent 方法相結合可以獲得該元素到 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:

元素在水平方向上佔用的空間大小:
offsetWidth = border-left + padding-left + width + padding-right + border-right

offsetHeight:

元素在垂直方向上佔用的空間大小:
offsetHeight = border-top + padding-top + height + padding-bottom + border-bottom

注:如果存在垂直滾動條,offsetWidth 也包括垂直滾動條的寬度;如果存在水平滾動條,offsetHeight 也包括水平滾動條的高度;

offsetTop:

元素的上外邊框至 offsetParent 元素的上內邊框之間的畫素距離;

offsetLeft:

元素的左外邊框至 offsetParent 元素的左內邊框之間的畫素距離;

注意事項

  1. 所有偏移量屬性都是隻讀的;
  2. 如果給元素設定了 display:none,則它的偏移量屬性都為 0;
  3. 每次訪問偏移量屬性都需要重新計算(儲存變數);
  4. 在使用的時候可能出現 DOM 沒有初始化,就讀取了該屬性,這個時候會返回 0;對於這個問題我們需要等到 DOM 元素初始化完成後再執行。

遇到的兩個問題

一、吸頂的那一刻伴隨抖動

出現抖動的原因是因為:在吸頂元素 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 了。

二、吸頂效果不能及時響應

這個問題是我比較頭痛,之前我沒有在意過這個問題。直到有一天我用美團點外賣的時候,我才開始注意這個問題。

描述:

  1. 當頁面往下滾動時,吸頂元素需要等頁面滾動停止之後才會出現吸頂效果
  2. 當頁面往上滾動時,滾動到吸頂元素恢復文件流位置時吸頂元素不恢復原樣,而等頁面停止滾動之後才會恢復原樣

原因: 在 ios 系統上不能實時監聽 scroll 滾動監聽事件,在滾動停止時才觸發其相關的事件。

解決方案:

還記得第一種方案中的 position:sticky 嗎?這個屬性在 IOS6 以上的系統中有良好的相容性,所以我們可以區分 IOS 和 Android 裝置做兩種處理。

IOS 使用 position:sticky,Android 使用滾動監聽 getBoundingClientRect().top 的值。

如果 IOS 版本過低呢?這裡提供一種思路:window.requestAnimationFrame()

前端詞典系列

《前端詞典》這個系列會持續更新,每一期我都會講一個出現頻率較高的知識點。希望大家在閱讀的過程當中可以斧正文中出現不嚴謹或是錯誤的地方,本人將不勝感激;若通過本系列而有所得,本人亦將不勝欣喜。

內容: 前端以及網路相關知識點的介紹並加以實際應用作為輔助。

目的: 這個系列的文章可以對讀者起到一點幫助,解開一些迷惑。

希望各位多指點一二,不吝賜教。

如果你覺得我的文章寫的還不錯,可以關注我的微信公眾號,公眾號裡會提前劇透呦。

【前端詞典】4 種滾動吸頂實現方式的比較

熱門文章傳送門

  1. 【前端詞典】滾動穿透問題的解決方案
  2. 【前端詞典】繼承(一) - 面試官問的你都會嗎?
  3. 【前端詞典】進階必備的網路基礎(上)
  4. 【前端詞典】進階必備的網路基礎(下)
  5. 【前端詞典】實現 Canvas 下雪背景引發的效能思考
  6. 【前端詞典】從輸入 URL 到展現涉及哪些快取環節(非常詳細)

相關文章