1.為什么使用虛擬滾動(dòng)?
首先提到一個(gè)現(xiàn)象,前端的性能瓶頸那就是頁(yè)面的卡頓,當(dāng)然這種頁(yè)面的卡頓包含了多種原因。例如HTTP請(qǐng)求過(guò)多導(dǎo)致數(shù)據(jù)加載國(guó)漫,下載的靜態(tài)文件非常大導(dǎo)致頁(yè)面加載時(shí)間很長(zhǎng),js中一些算法響應(yīng)的時(shí)間過(guò)長(zhǎng)等。很多前端工程師都花費(fèi)很多的精力在dom渲染上來(lái)優(yōu)化頁(yè)面加載。
2.瀏覽器渲染原理
在我們討論今天的這個(gè)虛擬滾動(dòng)原理之前需要了解一下瀏覽器的渲染原理。
瀏覽器渲染頁(yè)面的過(guò)程分為以下幾步:
解析html文件并生成 Dom Tree。
CSS解析生成CSS Rule Tree。
在渲染階段,瀏覽器會(huì)把DOM Tree 和 CSS Rule Tree 給DOM Tree上的每個(gè)節(jié)點(diǎn)添加樣式,并生成Render Tree。
Render Tree(layout/reflow),繪制元素尺寸、位置計(jì)算。
將計(jì)算好的信息發(fā)給GPU并顯示在頁(yè)面。
具體的瀏覽器原理在這篇文章不做過(guò)多的介紹,有興趣的話(huà)可以去看我的另一篇文章《一篇文章理解瀏覽器渲染原理和機(jī)制》。
3.瀏覽器渲染瓶頸
首先大家要明重繪和回流(重排)的概念:
重繪(repaint):當(dāng)Render Tree 中的一些元素需要更新元素本身的屬性,只影響外觀樣式和顏色等,不影響整個(gè)布局。
回流(reflow):當(dāng)Render Tree 中的某些元素因?yàn)橐?guī)模、尺寸、位置等改變時(shí),會(huì)影響整個(gè)布局。
回流必定發(fā)生重繪,重繪不一定發(fā)生回流,所以大家可以知道,回流所造成的影響是比較大的,如果頁(yè)面中頻繁的觸發(fā)回流的操作,那么最終造成頁(yè)面卡頓也是肯定的。
造成回流和重繪的操作有以下類(lèi)別:
頁(yè)面初始化
添加或者刪除頁(yè)面上的可視區(qū)DOM元素
元素位置發(fā)生改變,定位和浮動(dòng),盒模型
頁(yè)面文本內(nèi)容發(fā)生變化,影響輸入框的大小改變。
圖片顯示加載,如果沒(méi)有加載圖片又會(huì)被替換成相應(yīng)提示文字信息。
瀏覽器窗口尺寸大小變化(回流是根據(jù)視口大小來(lái)計(jì)算頁(yè)面元素的位置和大小)。
其實(shí)對(duì)于這些需要考慮的因素,一些瀏覽器也是做出了相應(yīng)的處理,因?yàn)槊看位亓骺赡軙?huì)造成巨大的影響,瀏覽器本身會(huì)實(shí)現(xiàn)一個(gè)隊(duì)列記錄每次回流時(shí)操作,當(dāng)存放的操作數(shù)量達(dá)到一定值或者達(dá)到一定時(shí)間后會(huì)對(duì)隊(duì)列中的操作進(jìn)行清空,并一次性進(jìn)行一次回流,讓多次回流操作壓縮成一次回流操作執(zhí)行,提高效率。
本文章將著重講述關(guān)于滾動(dòng)事件scroll event造成的影響。滾動(dòng)事件本身不會(huì)造成太多的性能消耗,而是因?yàn)闈L動(dòng)事件伴隨有大量的元素參與進(jìn)來(lái)一起進(jìn)行回流的操作才會(huì)影響瀏覽器的性能。例如一個(gè)表格有上萬(wàn)行數(shù)據(jù),如果一次性展示在頁(yè)面中,并且在滾動(dòng)時(shí)顯示對(duì)應(yīng)偏移的數(shù)據(jù),那么每一次滾動(dòng)都會(huì)對(duì)這幾萬(wàn)個(gè)元素進(jìn)行回流,那么性能肯定會(huì)很差。
瀏覽器的瓶頸主要在于:
1.無(wú)法一次性渲染太多的DOM元素。
2.每次滾動(dòng)事件將會(huì)讓對(duì)應(yīng)的DOM中所有元素重新渲染。
針對(duì)于瀏覽器的瓶頸問(wèn)題,有三種解決辦法:數(shù)據(jù)分頁(yè)、無(wú)限滾動(dòng)、虛擬滾動(dòng)。
4.數(shù)據(jù)分頁(yè)
許多網(wǎng)頁(yè)和應(yīng)用程序都會(huì)用到這樣的方,對(duì)需要展示的大量數(shù)據(jù)進(jìn)行分割分頁(yè),后端已經(jīng)做好了分頁(yè),前端只需要調(diào)用后端的接口傳入相應(yīng)的第幾頁(yè)的參數(shù)就能獲取到,減少了一次性需要渲染的行數(shù),但是如果查詢(xún)的表列數(shù)非常多,還是可能會(huì)渲染很多元素,不是一個(gè)很穩(wěn)定的方法。
5.無(wú)限滾動(dòng)
該方法是在頁(yè)面渲染一次性所能成手最大范圍的數(shù)據(jù)量,當(dāng)滾動(dòng)條快接近底部時(shí),再去追加渲染下一批需要渲染的元素,但是該方法的明顯缺血在于,如果數(shù)據(jù)量過(guò)大,無(wú)限滾動(dòng)下去那么最終所造成渲染的元素越來(lái)越多,性能也不會(huì)很好。
6.虛擬滾動(dòng)
虛擬滾動(dòng)其實(shí)就是綜合數(shù)據(jù)分頁(yè)和無(wú)限滾動(dòng)的方法,在有限的視口中只渲染我們所能看到的數(shù)據(jù),超出視口之外的數(shù)據(jù)就不進(jìn)行渲染,可以通過(guò)計(jì)算可視范圍內(nèi)的但單元格,保證每一次滾動(dòng)渲染的DOM元素都是可以控制的,不會(huì)擔(dān)心像數(shù)據(jù)分頁(yè)一樣一次性渲染過(guò)多,也不會(huì)發(fā)生像無(wú)限滾動(dòng)方案那樣會(huì)存在數(shù)據(jù)堆積,是一種很好的解決辦法。
假設(shè)實(shí)際開(kāi)發(fā)中服務(wù)端一次響應(yīng)20萬(wàn)條列表數(shù)據(jù),此時(shí)設(shè)備屏幕只允許容納20條,那么用戶(hù)理論上只可以看見(jiàn)20條數(shù)據(jù),其他的數(shù)據(jù)不會(huì)進(jìn)行渲染加載。如果前端將20萬(wàn)條數(shù)據(jù)全部渲染成DOM元素,可能造成程序卡頓,占用較大資源,非常影響用戶(hù)體驗(yàn),那么虛擬滾動(dòng)技術(shù)就完美的解決了這一問(wèn)題。

如圖所示,當(dāng)我們進(jìn)行滾動(dòng)時(shí),可視區(qū)域大小不變,渲染的元素?cái)?shù)量也是可以控制的,合理的減少了不必要的DOM渲染,提高瀏覽器的性能。
可以計(jì)算:卷入行數(shù) = scrollTop(卷入高度) / 每行的高度(itemH)
黃色邊框內(nèi)為可視區(qū)域,可視區(qū)域內(nèi)的紅色行表示在頁(yè)面能展示的數(shù)據(jù),每次滾動(dòng)時(shí),計(jì)算scrollTop的值,可視區(qū)域內(nèi)的紅色渲染部分高度可以略大于黃色邊框可是高度,避免滾動(dòng)的時(shí)候直接替換。
如何計(jì)算可視區(qū)域渲染的元素以及實(shí)現(xiàn)虛擬滾動(dòng),步驟如下:
統(tǒng)一設(shè)置每一行的高度需要相同,方便計(jì)算。
需要計(jì)算渲染數(shù)據(jù)數(shù)量(數(shù)組的長(zhǎng)度),根據(jù)每行的高度以及元素的總量計(jì)算整個(gè)DOM渲染容器的高度。
獲取可視區(qū)域的高度
觸發(fā)滾動(dòng)事件后,計(jì)算偏移量(滾動(dòng)條據(jù)頂距離),再根據(jù)可視區(qū)域高度計(jì)算本次偏移的截止量,得到需要渲染的具體數(shù)據(jù)。
對(duì)于與表格的列來(lái)說(shuō),需要做虛擬滾動(dòng)的話(huà),在x軸同樣可以根據(jù)以上步驟執(zhí)行,實(shí)現(xiàn)橫向虛擬滾動(dòng)。
7.自定義封裝一個(gè)虛擬滾動(dòng)組件:
子組件:
<template>
<!-- 可視區(qū)盒子 -->
<div :style="`height:${viewH}px;overflow-y:scroll`"
@scroll="handleScroll"
class="container">
<div :style="`height:${scrollH}px`"
class="list">
<div class="item_box"
:style="`transform:translateY(${offsetY}px)`">
<div class="item"
:style="`height:${itemH}px`"
v-for="(item,index) in list"
:key="index">
{{ item }}
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'ScrollComponent',
props: {
data: Array, // 列表總數(shù)據(jù)
viewH: Number, // 外部高度
itemH: Number, // 單項(xiàng)高度
},
data () {
return {
scrollH: '', // 整個(gè)滾動(dòng)列表高度(總高度)
list: [], // 每次顯示的數(shù)據(jù)
showNum: '', // 頁(yè)面需要顯示的數(shù)量
offsetY: '',// 動(dòng)態(tài)偏移量- 外層的盒子進(jìn)行滾動(dòng)設(shè)置
lastTime: '', //最新的時(shí)間
}
},
mounted () {
// 初始化計(jì)算
this.scrollH = this.data.length * this.itemH
// 計(jì)算可視化高度中能存幾個(gè)列表,可以略多余可視化高度能存放的列表數(shù)量避免滾動(dòng)時(shí)被替換
this.showNum = Math.floor(this.viewH / this.itemH) + 1
// 默認(rèn)展示的幾個(gè)數(shù)據(jù)
this.list = this.data.slice(0, this.showNum)
this.lastTime = new Date().getTime()
},
methods: {
// handleScroll 滾動(dòng)時(shí)候觸發(fā)回調(diào)
handleScroll (e) {
// 控制滾動(dòng)時(shí)間間隔
if (new Date().getTime() - this.lastTime > 10) {
let scrollTop = e.target.scrollTop //滾動(dòng)條高度
// 每一次滾動(dòng)后 根據(jù)scrollTop值獲取一個(gè)可以整除itemH結(jié)果進(jìn)行偏移
// 例如:scrollTop = 1220,1220 % this.itemH = 20 offsetY = 1220-20 = 1200
this.offsetY = scrollTop - (scrollTop % this.itemH)
console.log('卷入scrollTop值:', scrollTop, '卷入的行數(shù):', scrollTop % this.itemH);
this.list = this.data.slice(
Math.floor(scrollTop / this.itemH), // 計(jì)算卷入了多少行
Math.floor(scrollTop / this.itemH) + this.showNum
)
this.lastTime = new Date().getTime() //更新最新時(shí)間
}
}
}
}
</script>
<style scoped>
.container {
position: relative;
top: 200px;
left: 500px;
border: 1px solid red;
width: 500px;
}
.item {
border: 1px solid pink;
}
</style>
?
父組件:
<template>
<div id="app">
<ScrollComponent :data="dataList"
:viewH="viewH"
:itemH="itemH" />
</div>
</template>
<script>
import ScrollComponent from './components/ScrollComponent.vue'
export default {
name: 'App',
components: {
ScrollComponent
},
data () {
return {
dataList: [1, 1, 11, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 11, 1, 1, 1, 1, 11, 1, 1, 1, 1, 1],
viewH: 200,
itemH: 40
}
},
mounted () {
}
}
</script>
<style>
* {
margin: 0;
padding: 0;
}
</style>
?文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-452332.html
瀏覽器顯示結(jié)果:文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-452332.html

到了這里,關(guān)于一篇文章理解虛擬滾動(dòng)原理的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!