虛擬列表是怎麼做效能最佳化的?

熊的貓發表於2023-01-19

前言

一個簡單的情景模擬(千萬別被帶入)050866B0.gif

A: 假設現在有 10 萬條資料,你作為前端該怎麼最佳化這種大資料的列表?

B: 針對大資料列表一般不會依次性載入,會採用上拉載入、分頁載入等方式實現最佳化.

A: 那假如載入到最後一條資料的時候,頁面上只是列表部分的資料就至少對應 10 萬個 dom 節點,你覺得一個頁面渲染至少 10 萬個 dom 節點的效能如何?

A: 如果這樣的列表有 n 個呢?還有沒有別的最佳化方式?

B: 要不我把自己最佳化一下 ......

其實解決上述問題就可以使用 虛擬滾動 來實現最佳化,相信大家對這個詞都不陌生,但由於這個名詞比較短(又是虛擬又是滾動)導致很多人覺得這是非常高大上、難以理解的內容,但其實恰恰相反。

本文的目的就是幫助你以 最簡單 的方式 理解虛擬滾動,甚至實現 虛擬滾動.

虛擬滾動(Virtual Scrolling)

核心最佳化的方式:

限定文件中渲染列表數量,即固定渲染的 DOM 數,透過動態切換資料內容實現檢視的更新,並保證文件中真實 DOM 的數量不隨著資料量增大而增大.

050866B0.gif

理解虛擬滾動

其實要理解 虛擬滾動 這個詞很簡單,按照 虛擬滾動 兩部分來理解就很簡單了,下面就一一拆解。

虛擬

通常在頁面列表中,要渲染的 列表數量 和真實在文件中存在的 DOM 節點數1 : 1 的。053E8211.gif

每個列表項都擁有相同的高度(假設是 30px),這個列表容器中需要完全渲染的列表數(假設是 100 條)和在頁面中的高度是一致的,即此時的高度就為 100 * 30 = 3000,對應列表的 DOM 數量為 100,如:

050866B0.gif

對於 虛擬滾動 來講,虛擬 的意思是指實際要渲染完整列表對應的高度是透過 虛擬計算 的,並不是指文件中存在對應的 DOM 節點數

上面的栗子對應到虛擬滾動來講,就意味著實際渲染完整列表對應的高度就仍為 100 * 30 = 3000,但實際渲染數就變為 10 條,關係圖大致如下:

050866B0.gif

滾動

所謂 滾動 就很好理解了,因為列表可視區通常會限制一定的高度,即 列表可視區高度,那麼此時只要 虛擬列表高度 值大於 列表可視區高度 時,就會產生捲軸即可發生滾動操作。

值得注意的是,在發生滾動時需要對 實際渲染的列表 進行一些處理,否則會出現 實際渲染的列表虛擬列表區 脫離的情況,比如:

050866B0.gif

關鍵點就是實現在發生 滾動 操作時,保證 實際渲染的列表 一直存在 列表可視區 中,並且動態切換需要渲染的列表資料。

實現虛擬滾動

核心步驟

  • 設定列表可視區的高度 containerHeight
  • 設定單個列表項的高度 listItemHeight
  • 計算渲染完整列表需要的高度 virtualHeight,即 virtualHeight = listItemHeight * data.length
  • 設定真實渲染資料的起始索引 startIndex、endIndex,用於從列表資料 data 中獲取對應的資料內容
  • 註冊/監聽滾動事件 onScroll

    • 獲取當前實際滾動距離 eleScrollTop
    • eleScrollTop 作為 translateY 的值,即 實際渲染列表元素 平移的數值,保證 實際渲染列表元素 一直存在可視區中
    • 根據實際的滾動距離 eleScrollTop,動態計算列表新的起始索引 startIndex、endIndex

效果預覽

050866B0.gif

具體實現細節都在如下的程式碼中,可結合其中的註釋閱讀:

// App.vue
<script>
const list = (num = 10)=> {
  const data = [];
  for (let i = 0; i < num; i++) {
    data.push({
      id: i+1,
      name: `第 ${i+1} 條列表`
    });
  }
  return data;
}
</script>

<template>
  <VirtualScroll :data="list(100)" />
</template>

// VirtualScroll.vue
<template>
  <!-- 虛擬滾動內容 -->
  <div
    class="virtual-scroller"
    @scroll="onScroll"
    :style="{ height: containerHeight + 'px' }"
  >
    <!-- 實際渲染的列表內容 -->
    <ul
      class="real-list-content"
      :style="{ transform: `translateY(${translateY}px)` }"
    >
      <li
        v-for="item in visibleList"
        :key="item.id"
        :style="{
          height: `${listItemHeight}px`,
          'line-height': `${listItemHeight}px`,
        }"
      >
        <div>{{ item.name }}</div>
      </li>
    </ul>

    <!-- 虛擬列表元素 -->
    <div class="virtual-height" :style="{ height: virtualHeight + 'px' }">
      ~ 資料載入完畢 ~
    </div>
  </div>
</template>

<script>
export default {
  name: "vue-virtual-scroll",
};
</script>
<script setup language="ts">
import { computed, ref } from "vue";

const props = defineProps({
  data: {
    type: Array,
    default: [],
  },
  startIndex: {
    type: Number,
    default: 0,
  },
  endIndex: {
    type: Number,
    default: 10,
  },
  listItemHeight: {
    type: Number,
    default: 60,
  },
  containerHeight: {
    type: Number,
    default: 500,
  },
});

let { data, listItemHeight } = props;

const translateY = ref(0); // 平移距離
const startIndex = ref(props.startIndex); // 開始索引
const endIndex = ref(props.endIndex); // 結束索引

// 實際渲染的資料
const visibleList = computed(() => {
  return data.slice(startIndex.value, endIndex.value);
});

// 虛擬滾動的高度
const virtualHeight = computed(() => {
  return (data.length - visibleList.value.length) * listItemHeight + listItemHeight;
});

// 滾動事件
const onScroll = (e) => {
  const eleScrollTop = e.target.scrollTop;
  // 保證實際渲染列表一直停留在可視區
  translateY.value = eleScrollTop;
  // 根據實際的滾動距離,動態計算列表開始索引
  startIndex.value = Math.floor(eleScrollTop / listItemHeight);
  // 基於開始索引
  endIndex.value = startIndex.value + 10;
};
</script>

<style scoped>
.virtual-scroller {
  border: solid 1px #eee;
  margin-top: 10px;
  height: 600px;
  overflow: auto;
}

.virtual-height {
  background: red;
  display: flex;
  align-items: end;
  justify-content: center;
  color: #fff;
}

ul {
  list-style: none;
  padding: 0;
  margin: 0;
}

li {
  outline: solid 1px #fff;
  background-color: #000;
  color: #fff;
}
</style>

最後

其實 虛擬滾動 並不難理解,就像 CSS 中的 BFCJavaScript 中的閉包 等概念一樣,最初瞭解時你很難給它一個定義,但是實際上下功夫去了解它,其實也就那麼一回事。

以上的實現方式是極簡的方式,沒有做任何的最佳化、沒有考慮額外的場景,因為本文的目的還是想透過最簡單的實現去解釋虛擬滾動到底是怎麼一回事,因此不必過於糾結,當然基於 vue 相關庫的實現如 vue-virtual-scroller 可自行了解。

相關文章