移動端滾動研究

移動web滾動問題

在移動端若是使用局部滾動,意思就是咱們的滾動在一個固定寬高的div內觸發,將該div設置成overflow:scroll/auto;來造成div內部的滾動,這時咱們監聽div的onscroll發現觸發的時機區分android和ios兩種狀況,具體能夠看下面表格:css

機型(內核) body滾動 局部滾動
ios 不能實時觸發 不能實時觸發
android 實時觸發 實時觸發
ios wkwebview內核 實時觸發 實時觸發

不能實時觸發表現:只在手指觸摸的屏幕上一直滑動時和滾動中止的那一刻才觸發。android

關於模擬滾動

概念

正常的滾動:咱們平時使用的scroll,包括上面講的滾動都屬於正常滾動,利用瀏覽器自身提供的滾動條來實現滾動,底層是由瀏覽器內核控制。ios

模擬滾動:最典型的例子就是iscroll了,原理通常有兩種:web

  • 監聽滾動元素的touchmove事件,當事件觸發時修改元素的transform屬性來實現元素的位移,讓手指離開時觸發touchend事件,而後採用requestanimationframe來在一個線型函數下不斷的修改元素的transform來實現手指離開時的一段慣性滾動距離。
  • 監聽滾動元素的touchmove事件,當事件觸發時修改元素的transform屬性來實現元素的位移,讓手指離開時觸發touchend事件,而後給元素一個css的animation,並設置好duration和function來實現手指離開時的一段慣性距離。

方案比較

第一種方案因爲慣性滾動的時機時由js本身控制因此能夠拿到滾動觸發階段的scrolltop值,而且滾動的回調函數onscroll在滾動的階段都會觸發。第二種方案相比第一種要劣勢一些,區別在於手指離開時,採用的時css的animation來實現慣性滾動,因此沒法直接觸發慣性滾動過程當中的onscroll事件,只有在animation結束時才能夠藉助animationend來獲取到事件,固然也有一種方法能夠實時獲取滾動事件,也是藉助於requestanimationframe來不斷的去讀取滾動元素的transform來拿到scrolltop同時觸發onscroll回調。ajax

正常滾動和模擬滾動的性能比較

模擬滾動的fps值波動較大,這樣滾動起來會有明顯的卡頓感受,各位體驗的時候若是滾動超過10屏以後就能夠明顯感受到兩着的區別。瀏覽器

在使用模擬滾動時,瀏覽器在js層面會消耗更多的性能去改變dom元素的位置,在dom複雜層級深的頁面更爲高,因此在長列表滾動時還要使用正常滾動更好。bash

滾動和下拉刷新

方案1:藉助iscroll的原理,整個頁面使用模擬滾動,將下拉刷新元素放在頂部,當頁面滾動到頂部下拉時,下拉刷新元素隨着頁面的滾動出現,當手指離開時收回,此方案實現起來較爲簡單直接藉助iscoll便可,可是使用了模擬滾動以後在正常的列表滾動時性能上不如正常滾動。dom

方案2:頁面使用正常滾動,將下拉刷新元素放置在頂部top值爲負值(正常狀況下不可見),當頁面處於頂部時下拉,這時監聽touchmove事件,修改scrollcontent的tranlateY值,同時修改下拉刷新元素的tranlateY值,將二者同時位移來將下拉刷新元素顯示出來,手指離開時(touchend)收回,這種方案知足了在正常列表滾動時使用原生的滾動節省性能,只在下拉刷新時使用模擬滾動來實現效果。函數

方案3:方案2的改良版,惟一不一樣是將下拉刷新元素和scrollcontent放在一個div裏,將下拉刷新元素的margintop設爲負值,在下拉刷新時,只須要修改scrollcontent一個元素的tranlateY值便可實現下拉,在性能上要比方案2好。佈局

還會有一個性能上的問題就是:當頁面的列表過長,dom元素過多時,在模擬滾動,下拉刷新這段時間內,頁面也會有卡頓現象,這裏採起了一個優化策略即:

  • 列表較長時dom數量較多時,在觸發下拉刷新的時機時將頁面視窗以外的dom元素隱藏或者存放在fragment裏面。
  • 在刷新完成以後手指離開(touchend)時將隱藏的元素顯示出來。
  • 須要注意的是,隱藏和顯示視窗外的元素這個操做在下拉刷新時只會執行一次,而且只有在下拉刷新時纔會執行。

下面介紹如何去優化scroll事件的觸發,避免scroll事件過分消耗資源:

防抖(Debouncing)和節流(Throttling)

scroll 事件自己會觸發頁面的從新渲染,同時 scroll 事件的 handler 又會被高頻度的觸發, 所以事件的 handler 內部不該該有複雜操做,例如 DOM 操做就不該該放在事件處理中。 特別是針對此類高頻度觸發事件問題(例如頁面 scroll ,屏幕 resize,監聽用戶輸入等)。

防抖(Debouncing)

防抖技術便是能夠把多個順序地調用合併成一次,也就是在必定時間內,規定事件被觸發的次數。

節流(Throttling)

防抖函數確實不錯,可是也存在問題,譬如圖片的懶加載,我但願在下滑過程當中圖片不斷的被加載出來,而不是隻有當我中止下滑時候,圖片才被加載出來。又或者下滑時候的數據的 ajax 請求加載也是同理。這個時候,咱們但願即便頁面在不斷被滾動,可是滾動 handler 也能夠以必定的頻率被觸發(譬如 250ms 觸發一次),這類場景,就要用到另外一種技巧,稱爲節流函數(throttling)。

節流函數,只容許一個函數在 X 毫秒內執行一次。

與防抖相比,節流函數最主要的不一樣在於它保證在 X 毫秒內至少執行一次咱們但願觸發的事件 handler。

關於防抖動與節流,個人博客文章也有說起。

使用rAF(requestAnimationFrame)觸發滾動事件

若是頁面只須要兼容高版本瀏覽器或應用在移動端,又或者頁面須要追求高精度的效果,那麼可使用瀏覽器的原生方法 rAF(requestAnimationFrame)。

window.requestAnimationFrame() 這個方法是用來在頁面重繪以前,通知瀏覽器調用一個指定的函數。這個方法接受一個函數爲參,該函數會在重繪前調用。

rAF 經常使用於 web 動畫的製做,用於準確控制頁面的幀刷新渲染,讓動畫效果更加流暢,固然它的做用不只僅侷限於動畫製做,咱們能夠利用它的特性將它視爲一個定時器。(固然它不是定時器)

一般來講,rAF 被調用的頻率是每秒 60 次,也就是 1000/60 ,觸發頻率大概是 16.7ms 。(當執行復雜操做時,當它發現沒法維持 60fps 的頻率時,它會把頻率下降到 30fps 來保持幀數的穩定。)

var ticking = false; // rAF 觸發鎖
 
function onScroll(){
  if(!ticking) {
    requestAnimationFrame(realFunc);
    ticking = true;
  }
}
 
function realFunc(){
	// do something...
	console.log("Success");
	ticking = false;
}
// 滾動事件監聽
window.addEventListener('scroll', onScroll, false);
複製代碼

實現以16.7ms 觸發一次 handler,下降了可控性,可是提高了性能和精確度。

從本質上而言,咱們應該儘可能去精簡 scroll 事件的 handler ,將一些變量的初始化、不依賴於滾動位置變化的計算等都應當在 scroll 事件外提早就緒。

避免在scroll 事件中修改樣式屬性 / 將樣式操做從 scroll 事件中剝離

alt text

輸入事件處理函數,好比 scroll / touch 事件的處理,都會在 requestAnimationFrame 以前被調用執行。

所以,若是你在 scroll 事件的處理函數中作了修改樣式屬性的操做,那麼這些操做會被瀏覽器暫存起來。而後在調用 requestAnimationFrame 的時候,若是你在一開始作了讀取樣式屬性的操做,那麼這將會致使觸發瀏覽器的強制同步佈局。

滑動過程當中嘗試使用 pointer-events: none 禁止鼠標事件

pointer-events 是一個 CSS 屬性,能夠有多個不一樣的值,大概的意思就是禁止鼠標行爲,應用了該屬性後,譬如鼠標點擊,hover 等功能都將失效,便是元素不會成爲鼠標事件的 target。

pointer-events: none 可用來提升滾動時的幀頻。的確,當滾動時,鼠標懸停在某些元素上,則觸發其上的 hover 效果,然而這些影響一般不被用戶注意,並多半致使滾動出現問題。對 body 元素應用 pointer-events: none ,禁用了包括 hover 在內的鼠標事件,從而提升滾動性能。

大概的作法就是在頁面滾動的時候, 給 添加上 .disable-hover 樣式,那麼在滾動中止以前, 全部鼠標事件都將被禁止。當滾動結束以後,再移除該屬性。

// css 代碼
.disable-hover,
.disable-hover * {
  pointer-events: none !important;
}
// js 代碼
var body = document.body,
    timer;
window.addEventListener('scroll', function() {
  clearTimeout(timer);
  if(!body.classList.contains('disable-hover')) {
    body.classList.add('disable-hover')
  }
  timer = setTimeout(function(){
    body.classList.remove('disable-hover')
  },500);
}, false);
複製代碼

參考 移動 Web 的滾動,高性能滾動及頁面渲染優化

相關文章
相關標籤/搜索