相信你們都遇到過渲染一個很長的列表或者頁面帶來的痛苦,長列表與頁面可能對首屏渲染速度形成很大的影響,而且會對頁面的滾動形成一些不流暢的體驗。html
我也在最近遇到了這個問題,發現除了直接使用分頁外,虛擬滾動這種解決方案非常流行,因而也從新造了一下vue中虛擬滾動的輪子。虛擬滾動簡單的說就是渲染在瀏覽器中當前可見的範圍內的內容,經過用戶滑動滾動條的位置動態地來計算顯示內容,其他部分用空白填充來給用戶形成一個長列表的假象。前端
這個輪子其實並無你們想象中的複雜,下面就具體介紹一下這個輪子是怎麼造的,若是你們在工做中學習中遇到一樣的場景也能夠適用這種解決方案。如下即是我實現的虛擬滾動的一個簡單demo,在線demo與源碼。vue
虛擬滾動的核心dom結構其實就是一個簡單的列表,在vue中可被描述爲以下的代碼git
<div style="overflow-y: scroll; height: 300px;" @scroll="handleScroll">
<div v-for="item in items" :key="`${item.id}`">
<slot :data="item">
</slot>
</div>
</div>
複製代碼
這裏用了vue的scoped slot
來處理用戶的自定義dom內容與自定義dom內容的傳入數據,若是沒有scoped slot
,咱們也能夠經過讓用戶在傳入數據時,在傳入的數據對象中定義一個特定的渲染函數來實現這一步驟。github
有了這個可自定義dom的列表結構後,在外面套一層可滾動的定高的容器,咱們就實現了一個全部列表類組件的基礎dom。那麼接下來要作的就是填充可視列表之外的滾動高度。這個作法有挺多的,好比在列表上下定義<div>
,經過改動<div>
高度來控制總高度;好比經過控制列表的padding-top
與padding-bottom
來控制;再好比直接將列表高度設置成全部元素高度總和,經過定義position
啓用top
來進行定位也是能夠的......那麼有了這個dom結構後咱們就能夠來對顯示內容進行計算了。數組
首先咱們能夠肯定的是列表的可視範圍是一段連續的數組內容,因而這個計算就被簡化爲了找到連續數組內容的開始點與結束點。開始點與結束點依賴的兩個信息:一是列表每一項的具體y座標,二就是當前可視範圍的開始點與結束點。列表每一項的y座標能夠用一次循環經過累加每項的高度來獲得每項的y座標,以下圖瀏覽器
當前可視範圍的開始點s
便是列表容器的scrollTop
屬性,而結束點e
就是s
加上列表容器的高度。如今咱們有了計算數組開始點與結束點的全部信息了,數組的開始點計算就是在全部項的y座標中尋找到一個不大於可視範圍開始點s
的項,數組的結束點計算就是在全部項的y座標中尋找一個不小於e
的項,以下圖緩存
當數組每一項都爲非固定高度的時候,咱們採用二分法(具體實現可參看源碼)來尋找數組的上界與下界;當數組每一項爲固定高度的時候,咱們能夠直接用s
除以每項高度向下取整(floor)來獲得上界,用e
除以每項高度向上取整(ceil)來獲得下界。而後用slice
方法得到最終須要展現的元素數組。框架
可能你們注意到了,雖然咱們用了二分法O(logN)
與直接計算O(1)
代替了普通的遍從來尋找上下界,可是slice
方法仍是會將總體的複雜度提高到O(N)
,因此這個優化也僅僅對在有限數組的狀況起到必定的提速做用。那麼在實際應用中咱們會不會遇到一個至關龐大的數組,大到可以忽視O(logN)
與O(1)
帶來的提高呢?答案是否認的,由於瀏覽器對頁面的內存限制咱們很難在實際應用中趕上這樣一個數組。dom
有了數組的上界與下界,上下填充高度的計算其實很是直觀,咱們以設置列表的padding-top
與padding-bottom
屬性爲例,如圖
不少時候咱們須要優化的不是一個長列表,而是一個長頁面,那麼對於上述的計算方法有什麼改變呢?
首先咱們須要改變可視範圍開始點s
與結束點e
的計算方法,對於頁面而言可視範圍的開始點便是window.pageYOffset || document.documentElement.scrollTop
;結束點是開始點加上可視範圍的高度,這裏的高度計算咱們使用window.innerHeight || document.documentElement.clientHeight
,但請注意這兩個屬性在頁面有滾動條的時候返回的值是不一樣的,innerHeight
會包含滾動條的高度,clientHeight
不包含滾動條的高度。
計算完了可視範圍,咱們還須要調整數組y座標。原先的數組y座標都是相對於滾動容器而言的,如今咱們須要將數組的y座標調整爲相對於頁面。調整方法有兩種:一是能夠在計算y座標的時,加上滾動容器的offsetTop
屬性;二是能夠在計算可視範圍開始點s
與結束點e
時,減去滾動容器的offsetTop
屬性。
調整完了座標,咱們還須要將滾動容器的height
與overflow-y
屬性去掉,讓容器自由生長,同時將滾動容器的scroll
事件轉移到window
對象上,這樣就實現了對頁的虛擬滾動。經過頁模式,咱們就能夠實現對任何經過固定高度塊佈局的長頁面進行此類的優化。
當滾動刷新數據過於頻繁的時候,渲染就會就會產生閃爍,這時咱們就須要經過requestAnimationFrame
來調用更新列表的方法來實現對更新列表速率的控制,從而生成平滑的滾動動畫。
vue在這裏幫咱們處理了一部分列表更新的問題,好比在滾動形成的小範圍數組變更中,vue是會複用先前渲染的節點來進行列表更新的。若是你沒有使用相似的框架,那麼就須要本身去處理一下這部分的複用邏輯。
除此以外,咱們能夠對在必定範圍內的渲染內容直接進行緩存,例如咱們能夠限定緩存節點數量,在滾動時遇到緩存命中時直接使用緩存中的節點,若是無命中而且緩存節點已滿的狀況下則可用必定的緩存替換策略,例如用新節點來替換最不頻繁使用(LFU)的緩存節點。經過這樣的列表緩存來實現對小範圍滾動的再次優化。
咱們當前的作法依然是在滾動時對dom進行不停地銷燬與再建立,雖然每次建立與銷燬dom的開銷並不大,可是它們依舊會佔用瀏覽器的一部分性能。
當列表內的每個元素都是經過統一的dom模版或渲染函數進行渲染時,咱們就能夠經過列表回收的方式,將超出可視範圍的dom節點回收,再將新的數據注入到回收的dom節點中,最後將更新數據後的回收節點放回列表中去,以下圖。經過列表回收的方式能夠保證你的dom節點總量在一個極低的範圍內,而且省去了建立銷燬dom這一部分的開銷。
最後感謝你的閱讀,若是你們有什麼意見,建議或者前端相關的問題都歡迎與我交流,這是個人github,have a nice day! :)