結合“康熙選秀”,給大家講講“虛擬列表”

Sunshine_Lin發表於2022-01-13
覺得我講得好的,請幫忙點個贊,謝謝你們了。嘻嘻

場景

康熙選妃

image.png

image.png

話說這年是康熙五十三年,天下太平,天下無人不感嘆這“康熙盛世”啊,康熙自己也是開心的不得了啊,“朕奮鬥了大半輩子,還不能享樂享樂,傳命張廷玉來見我,我有事要讓他辦!”
  • 康熙:衡臣啊(衡臣是張廷玉的字),這康熙盛世如何
  • 張廷玉:皇上牛逼,皇上牛逼,皇上萬歲
  • 康熙:但是朕老了啊,但是朕不能服老,朕要證明給天下人看
  • 張廷玉:皇上正值壯年,萬歲萬萬歲
  • 康熙:我不管,我要選妃,我要選妃,我要選妃!!!
  • 張廷玉:我tm。。。你tm都60了還選?你扛得住嗎?大哥!
  • 康熙: 我不管,你給我去找,找一萬個妙齡女子進宮,我要選妃
  • 張廷玉:選個毛,你頂不動的- 康熙:我不管,我有鹿血,鹿血一杯,法力無邊
  • 張廷玉:不,你不行。
  • 康熙:你去不去?
  • 張廷玉:不去
  • 康熙: 你去不去
  • 張廷玉:我不去
  • 康熙:還想不想配享太廟了?
  • 張廷玉:皇上萬歲,臣一定上的聖託

    一個月後,一萬名妙齡女子進宮了。但是難題又來了。這麼多女子,不可能一次性讓康熙選吧,那不得花了他的眼睛。
張廷玉靈機一動:可以讓女子們分批進大殿讓皇上選嘛。具體可以這麼做:
  • 在皇上選妃的大殿外,再設定兩個偏殿
  • 宮女們分批次進大殿讓皇上看
  • 被看過的宮女們進左偏殿等待選妃結果,還沒排到的宮女在右偏殿等待

    這樣既提高了選秀效率,又可以讓皇上更輕鬆些。這樣做的好處就是:
  • 皇上不需要一次性看一萬個宮女,不用那麼勞累
  • 皇上如果選到一半累了,也可以休息,隔天再選,反正選到第幾批了,這些都已經記錄下了
  • 皇上如果某一天回想起哪個宮女還不錯,也可以往回查

多資料渲染

現在解決多資料渲染,相信大家可能會想到分頁,觸底載入,懶載入等等,但其實虛擬列表也是多資料高效能載入的一個重要解決方案。

虛擬列表的概念

虛擬滾動,就是根據容器可視區域列表容積數量,監聽使用者滑動或滾動事件,動態擷取長列表資料中的部分資料渲染到頁面上,動態使用空白站位填充容器上下滾動區域內容,模擬實現原生滾動效果
image.png
  • 瀏覽器渲染===康熙選秀:一次性渲染10000個肯定會使瀏覽器壓力大,造成使用者體驗差
  • 容器可視區域===選秀大殿:10000個排隊去渲染,比如一次渲染10個
  • 上方下方區域===左右偏殿:輪不到你渲染,你就乖乖進空白區待著

實現

基本實現

  • 可視區域的高度
  • 列表項的高度
  • 可視區域能展示的列表項個數 = ~~(可視區域高度 / 列表項高度) + 2
  • 開始索引
  • 結束索引
  • 預載入(防止滾動過快,造成暫時白屏)
  • 根據開始索引和結束索引,擷取資料展示在可視區域
  • 滾動節流
  • 上下空白區使用padding實現
  • 滑動到底,再次請求資料並拼接
<template>
  <div class="v-scroll" @scroll.passive="doScroll" ref="scrollBox">
    <div :style="blankStyle" style="height: 100%">
      <div v-for="item in tempSanxins" :key="item.id" class="scroll-item">
        <span>{{ item.msg }}</span>
        <img :src="item.src" />
      </div>
    </div>
  </div>
</template>


<script>
import { throttle } from "../../utils/tools";
export default {
  data() {
    return {
      allSanxins: [], // 所有資料
      itemHiehgt: 150, // 列表每一項的寬度
      boxHeight: 0, // 可視區域的高度
      startIndex: 0, // 元素開始索引
    };
  },
  created() {
    // 模擬請求資料
    this.getAllSanxin(30);
  },
  mounted() {
    // 在mounted時獲取可視區域的高度
    this.getScrollBoxHeight();
    // 監聽螢幕變化以及旋轉,都要重新獲取可視區域的高度
    window.onresize = this.getScrollBoxHeight;
    window.onorientationchange = this.getScrollBoxHeight;
  },
  methods: {
    getAllSanxin(count) {
      // 模擬獲取資料
      const length = this.allSanxins.length;
      for (let i = 0; i < count; i++) {
        this.allSanxins.push({
          id: `sanxin${length + i}`,
          msg: `我是三心${length + i}號`,
          // 這裡隨便選一張圖片就行
          src: require("../../src/asset/images/sanxin.jpg").default,
        });
      }
    },
    // 使用節流,提高效能
    doScroll: throttle(function () {
      // 監聽可視區域的滾動事件
      // 公式:~~(滾動的距離 / 列表項 ),就能算出已經滾過了多少個列表項,也就能知道現在的startIndex是多少
      // 例如我滾動條滾過了160px,那麼index就是1,因為此時第一個列表項已經被滾上去了,可視區域裡的第一項的索引是1
      const index = ~~(this.$refs.scrollBox.scrollTop / this.itemHiehgt);
      if (index === this.startIndex) return;
      this.startIndex = index;
      if (this.startIndex + this.itemNum > this.allSanxins.length - 1) {
        this.getAllSanxin(30);
      }
    }, 200),
    getScrollBoxHeight() {
      // 獲取可視區域的高度
      this.boxHeight = this.$refs.scrollBox.clientHeight;
    },
  },
  computed: {
    itemNum() {
      // 可視區域可展示多少個列表項? 計算公式:~~(視覺化區域高度 / 列表項高度) + 2
      // ~~是向下取整的運算子,等同於Math.floor(),為什麼要 +2 ,是因為可能最上面和最下面的元素都只展示一部分
      return ~~(this.boxHeight / this.itemHiehgt) + 2;
    },
    endIndex() {
      // endIndex的計算公式:(開始索引 + 可視區域可展示多少個列表項 * 2)
      // 比如可視區域可展示8個列表項,startIndex是0的話endIndex就是0 + 8 * 2 = 16,startIndex是1的話endIndex就是1 + 8 * 2 = 17,以此類推
      // 為什麼要乘2呢,因為這樣的話可以預載入出一頁的資料,防止滾動過快,出現暫時白屏現象
      let index = this.startIndex + this.itemNum * 2;
      if (!this.allSanxins[index]) {
         // 到底的情況,比如startIndex是99995,那麼endIndex本應該是99995 + 8 * 2 = 10011
        // 但是列表資料總數只有10000條,此時就需要讓endIndex = (列表資料長度 - 1)
        index = this.allSanxins.length - 1;
      }
      return index;
    },
    tempSanxins() {
      //   可視區域展示的擷取資料,使用了陣列的slice方法,不改變原陣列又能擷取
      let startIndex = 0;
      if (this.startIndex <= this.itemNum) {
        startIndex = 0;
      } else {
        startIndex = this.startIndex + this.itemNum;
      }
      return this.allSanxins.slice(startIndex, this.endIndex + 1);
    },
    blankStyle() {
      // 上下方的空白處使用padding來充當
      let startIndex = 0;
      if (this.startIndex <= this.itemNum) {
        startIndex = 0;
      } else {
        startIndex = this.startIndex - this.itemNum;
      }
      return {
        // 上方空白的高度計算公式:(開始index * 列表項高度)
        // 比如你滾過了3個列表項,那麼上方空白區高度就是3 * 150 = 450,這樣才能假裝10000個資料的滾動狀態
        paddingTop: startIndex * this.itemHiehgt + "px",
         // 下方空白的高度計算公式:(總資料的個數 - 結束index - 1) * 列表項高度
        // 例如現在結束index是100,那麼下方空白高度就是:(10000 - 100 - 1) * 150 = 1,484,850
        paddingBottom:
          (this.allSanxins.length - this.endIndex - 1) * this.itemHiehgt + "px",
          // 不要忘了加px哦
      };
    },
  },
};
</script>

<style lang="scss" scoped>
.v-scroll {
  height: 100%;
  /* padding-bottom: 500px; */
  overflow: auto;

  .scroll-item {
    height: 148px;
    /* width: 100%; */
    border: 1px solid black;
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 0 20px;

    img {
      height: 100%;
    }
  }
}
</style>

結語!

我是林三心,一個熱心的前端菜鳥程式設計師。如果你上進,喜歡前端,想學習前端,那我們們可以交朋友,一起摸魚哈哈,摸魚群,加我請備註【思否】

image.png

相關文章