簡易版虛擬滾動原理

簡易版虛擬滾動原理

1.為什么使用虛擬滾動?

首先提到一個(gè)現(xiàn)象,前端的性能瓶頸那就是頁面的卡頓,當(dāng)然這種頁面的卡頓包含了多種原因。例如HTTP請求過多導(dǎo)致數(shù)據(jù)加載國漫,下載的靜態(tài)文件非常大導(dǎo)致頁面加載時(shí)間很長,js中一些算法響應(yīng)的時(shí)間過長等。很多前端工程師都花費(fèi)很多的精力在dom渲染上來優(yōu)化頁面加載。

2.瀏覽器渲染原理

1.解析html文件并生成Dom Tree。
2.CSS解析生成CSS Rule Tree。
3.在渲染階段,瀏覽器會把DOM TreeCSS Rule TreeDOM Tree上的每個(gè)節(jié)點(diǎn)添加樣式,并生成Render Tree。
4.Render Tree(layout/reflow),繪制元素尺寸、位置計(jì)算。
將計(jì)算好的信息發(fā)給GPU并顯示在頁面。

3.瀏覽器渲染瓶頸
  • 重繪(repaint):當(dāng)Render Tree 中的一些元素需要更新元素本身的屬性,只影響外觀樣式和顏色等,不影響整個(gè)布局。

  • 回流(reflow):當(dāng)Render Tree 中的某些元素因?yàn)橐?guī)模、尺寸、位置等改變時(shí),會影響整個(gè)布局。

回流必定發(fā)生重繪,重繪不一定發(fā)生回流,所以大家可以知道,回流所造成的影響是比較大的,如果頁面中頻繁的觸發(fā)回流的操作,那么最終造成頁面卡頓也是肯定的。

造成回流和重繪的操作有以下類別:
  • 頁面初始化
  • 添加或者刪除頁面上的可視區(qū)DOM元素
  • 元素位置發(fā)生改變,定位和浮動,盒模型
  • 頁面文本內(nèi)容發(fā)生變化,影響輸入框的大小改變。
  • 圖片顯示加載,如果沒有加載圖片又會被替換成相應(yīng)提示文字信息。
  • 瀏覽器窗口尺寸大小變化(回流是根據(jù)視口大小來計(jì)算頁面元素的位置和大?。?/li>
瀏覽器的瓶頸主要在于:

1.無法一次性渲染太多的DOM元素。
2.每次滾動事件將會讓對應(yīng)的DOM中所有元素重新渲染。

針對于瀏覽器的瓶頸問題,有三種解決辦法:數(shù)據(jù)分頁、無限滾動、虛擬滾動。

4.數(shù)據(jù)分頁

對需要展示的大量數(shù)據(jù)進(jìn)行分割分頁,后端已經(jīng)做好了分頁,前端只需要調(diào)用后端的接口傳入相應(yīng)的第幾頁的參數(shù)就能獲取到,減少了一次性需要渲染的行數(shù),但是如果查詢的表列數(shù)非常多,還是可能會渲染很多元素,不是一個(gè)很穩(wěn)定的方法。

5.無限滾動

是在頁面渲染一次性所能承受最大范圍的數(shù)據(jù)量,當(dāng)滾動條快接近底部時(shí),再去追加渲染下一批需要渲染的元素,但是該方法的明顯缺點(diǎn)在于,如果數(shù)據(jù)量過大,無限滾動下去那么最終所造成渲染的元素越來越多,性能也不會很好。

6.虛擬滾動

虛擬滾動其實(shí)就是綜合數(shù)據(jù)分頁和無限滾動的方法,在有限的視口中只渲染我們所能看到的數(shù)據(jù),超出視口之外的數(shù)據(jù)就不進(jìn)行渲染,可以通過計(jì)算可視范圍內(nèi)的但單元格,保證每一次滾動渲染的DOM元素都是可以控制的,不會擔(dān)心像數(shù)據(jù)分頁一樣一次性渲染過多,也不會發(fā)生像無限滾動方案那樣會存在數(shù)據(jù)堆積,是一種很好的解決辦法。

假設(shè)實(shí)際開發(fā)中服務(wù)端一次響應(yīng)10萬條列表數(shù)據(jù),此時(shí)設(shè)備屏幕只允許容納20條,那么用戶理論上只可以看見20條數(shù)據(jù),其他的數(shù)據(jù)不會進(jìn)行渲染加載。如果前端將10萬條數(shù)據(jù)全部渲染成DOM元素,可能造成程序卡頓,占用較大資源,非常影響用戶體驗(yàn),那么虛擬滾動技術(shù)就完美的解決了這一問題。


image.png

如圖所示,當(dāng)我們進(jìn)行滾動時(shí),可視區(qū)域大小不變,渲染的元素?cái)?shù)量也是可以控制的,合理的減少了不必要的DOM渲染,提高瀏覽器的性能。

黃色邊框內(nèi)為可視區(qū)域,可視區(qū)域的紅色行表示在頁面能展示的數(shù)據(jù),每次滾動時(shí)計(jì)算scrollTop的值,可視區(qū)域內(nèi)的紅色渲染分高度可以略大于黃色邊框可視高度,避免滾動的時(shí)候直接替換。

如何計(jì)算可視區(qū)域的渲染的元素以及實(shí)現(xiàn)虛擬滾動步驟如下所示:

  • 統(tǒng)一設(shè)置每一行的高度(需要相同)方便計(jì)算
  • 需要計(jì)算渲染的數(shù)據(jù)量(數(shù)據(jù)的長度),根據(jù)每行的高度以及元素的總量計(jì)算整個(gè)Dom渲染容器的高度
  • 獲取可視區(qū)域的高度
  • 觸發(fā)滾動事件后,計(jì)算偏移量(滾動條距頂距離),再根據(jù)可視區(qū)域高度計(jì)算本次偏移的截至量,得到需要渲染的具體數(shù)據(jù)
7.虛擬滾動組件

父組件

<template>
  <div id="app">
    <ScrollComponents
      :data="dataList"
      :viewH="viewH"
      :itemH="itemH"
    ></ScrollComponents>
  </div>
</template>

<script>
import ScrollComponents from "@/views/scrcollList/scrollComponents.vue";
export default {
  name: "ScrollList",
  data() {
    return {
      dataList: [
        1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
        21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34,
      ],
      viewH: 400,
      itemH: 40,
    };
  },
  components: {
    ScrollComponents,
  },
};
</script>

<style scoped></style>

子組件

<template>
  <!-- 可視區(qū)盒子 -->
  <div
    class="container"
    :style="`height:${viewH}px;overflow-y:scroll`"
    @scroll="handleScroll"
  >
    <div class="list" :style="`height:${scrollH}px`">
      <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: "ScrollComponents",
  props: {
    data: Array, // 列表總數(shù)據(jù)
    viewH: Number, // 外部高度
    itemH: Number, // 單項(xiàng)高度
  },
  data() {
    return {
      scrollH: "", // 整個(gè)滾動列表高度(總高度)
      list: [], // 每次顯示的數(shù)據(jù)
      showNum: "", // 頁面需要顯示的數(shù)量
      offsetY: "", // 動態(tài)偏移量- 外層的盒子進(jìn)行滾動設(shè)置
      lastTime: "", //最新的時(shí)間
    };
  },
  methods: {
    //  滾動時(shí)候觸發(fā)回調(diào)
    handleScroll(e) {
      // 控制滾動時(shí)間間隔
      if (new Date().getTime() - this.lastTime > 10) {
        let scrollTop = e.target.scrollTop; //滾動條高度
        // 每一次滾動后 根據(jù)scrollTop值獲取一個(gè)可以整除itemH結(jié)果進(jìn)行偏移
        this.offsetY = scrollTop - (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í)間
      }
    },
  },
  mounted() {
    // 初始化計(jì)算
    this.scrollH = this.data.length * this.itemH;
    // 計(jì)算可視化高度中能存幾個(gè)列表,可以略多余可視化高度能存放的列表數(shù)量避免滾動時(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();
  },
};
</script>

<style scoped>
.container {
  position: relative;
  top: 200px;
  left: 100px;
  border: 1px solid red;
  width: 400px;
}
.item {
  border: 1px solid #008c8c;
}
</style>
瀏覽器顯示效果
image.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容