VueVirtualCollection的組件源碼分析如何作一個性能優秀的無限滾動加載

簡介

本篇文章解析了vue-virtual-collection組件是如何巧妙運用了「塊渲染」的思想去渲染須要的數據。javascript

能夠參考下圖:css

該圖片完美的解析了「塊渲染的思想」 , 讓咱們來分析一下上圖。html

爲了高效計算視圖中顯示那些塊,咱們能夠先定義一個 div 爲 X * X , 全部與這個div有重疊的Cell(塊)都會在這個塊記錄下來,而後把這個Cell(塊)保存到一個Map(至關與一個字典)中,那麼當滾動發生時咱們就能夠從這個Map(至關與一個字典)中找到當前須要渲染出來的塊,就不用再去遍歷全部的Cell(塊)再去進行渲染了。vue

試想下好比咱們須要在一本字典中找一個咱們想要找的單詞 , 咱們首先想到的是會有兩種方法去找。java

1.咱們去一頁一頁的去翻去查找。
2.咱們經過字典的索引去查找。react

上面兩種方法我會堅決果斷的選第二種,而"把這個Cell(塊)保存到一個Map中再去查找"道理是同樣的。git

此時,Map中記錄的應該是:github

{
  "0.0": [1, 2, 3, 5], // 0.0塊與1,2,3,5號Cell有重疊,下同
  "0.1": [5, 3, 6, 7],
  "0.2": [7, 6, 8, 9],
  "1.0": [2, 3, 4],
  "1.1": [3, 4, 6],
  "1.2": [6, 9]
}
複製代碼

當咱們滾動了頁面,根據滾動的距離、viewPort 的寬高,能夠很容易計算出當前須要渲染哪些塊。web

源碼解析

第一步咱們先去建立一個div爲包裹層app

App.vue
<template>
  <div class="vue-wrapper" :style="wrapperStyle">

  </div>
</template>

<script> export default { data () { return { height: 500, width: 500 } }, computed: { // 建立這個層的寬高 wrapperStyle () { return { height: `${this.height}px`, width: `${this.height}px` } } } } </script>
<style> .vue-wrapper { overflow: scroll; -webkit-overflow-scrolling: touch; } </style>
複製代碼

由於要設置咱們須要顯示那些數據,因此咱們須要爲每個塊都設置一些信息,表示當前塊包含哪些或者說與哪些塊是重疊在一塊兒的,因而咱們建立一個Section(類)

Section.js
/**
   *窗口的顯示部分 -> 當前viewPort顯示的部分。
   *把cell(塊)組合起來顯示在當前的窗口。
   *這使咱們可以更快地肯定在窗口的給定區域顯示哪些單元格。
   *顯示具備固定的大小,幷包含0到多個塊(由其索引跟蹤)。
 */

export default class Section {
  constructor ({width, height, x, y}) {
    this.width = width;
    this.height = height;
    this.x = x;
    this.y = y;

    // 收集當前應該顯示那些塊
    this._indexMap = {};

    // 收集當前須要顯示塊的索引
    this._indices = [];
  }

  // 有添加就有獲取

  // 添加塊的索引
  addCellIndex ({index}) {
    if (!this._indexMap[index]) {
      // 收集當前應該顯示那些塊
      this._indexMap[index] = true;
      // 收集當前須要顯示塊的索引並保持它們
     return this._indices.push(index);
    }
  }

  // 獲取全部塊的索引
  getCellIndex () {
    return this._indices;
  }
}

複製代碼

經過上面的類咱們已經爲每個塊都設置了一些信息,而後咱們再建立一個SectionManager(類)用於管理和設置這些塊,裏面包含了建立塊的索引,獲取塊,獲取塊索引的方法

// 用於建立每個塊所包含的信息
import Section from "./Section";

// 默認視圖大小 600
const SECTION_SIZE = 600;

export default class SectionManager {
  constructor (sectionSize = SECTION_SIZE) {
    // 設置默認視圖大小
    this._sectionSize = sectionSize;

    // 收集全部塊的數據
    this._cellMetadata = []
    
    // 用於收集一個塊所包含的信息
    this._sections = {};
  }
}
複製代碼

而後在該SectionManager(類)中定義一個方法用於建立一個塊的一些信息

// 建立一個塊裏面所應該包含的信息
  registerCell ({cellMetadatum, index}) {
    // 收集全部塊的數據
    this._cellMetadata[index] = cellMetadatum;
    
    // 該方法會返回全部的塊的信息
        this.getSections(cellMetadatum).forEach((section) => {
          return section.addCellIndex({index});
        });
  }
  
  // 該方法會返回全部的塊的信息
    getSections ({height, width, x, y}) {
      /*
       =>┏━━┯━━┯━━┓ 分割線sectionY
       0┃0 0 ┊1 3 ┊6 6 ┃
       1┃0 0 ┊2 3 ┊6 6 ┃
       =>┠┈┈┼┈┈┼┈┈┨ 分割線 sectionY
       2┃4 4 ┊4 3 ┊7 8 ┃
       3┃4 4 ┊4 5 ┊9 9 ┃
       ┗━━┷━━┷━━┛
       ↑    ↑
       sectionX sectionX
       */
  
      // 設置該塊X軸的分割線
      const sectionXStart = Math.floor(x / this._sectionSize);
      const sectionXStop = Math.floor((x + width - 1) / this._sectionSize);
  
      // 設置該塊Y軸的分割線
      const sectionYStart = Math.floor(y / this._sectionSize);
      const sectionYStop = Math.floor((y + height - 1) / this._sectionSize);
  
      // 設置用於保存全部重疊的塊
      const sections = [];
  
      // 建立塊的範圍
      for (let sectionX = sectionXStart; sectionX <= sectionXStop; sectionX++) {
        for (let sectionY = sectionYStart; sectionY <= sectionYStop; sectionY++) {
          // 爲每一塊都建立一個key用於查找
          const key = `${sectionX}.${sectionY}`;
          if (!this._sections[key]) {
            this._sections[key] = new Section({
              height: this._sectionSize,
              width: this._sectionSize,
              x: sectionX * this._sectionSize,
              y: sectionY * this._sectionSize
            });
          }
  
          // 把每一個塊所包含的信息都保存起來
          sections.push(this._sections[key])
        }
      }
  
      // 返回全部的塊的信息
      return sections
    }
複製代碼

而後咱們須要去調用這個類(SectionManager)的建立塊的方法去建立全部塊的所應該包含信息

App.vue
<script> created () { // 獲取塊的管理 this._sectionManager = new SectionManager(this.sectionSize); // 註冊塊和塊的管理 this.registerCellsToSectionManager(); }, methods: { // 註冊塊和塊的管理 registerCellsToSectionManager () { // 若是_sectionManager中沒有數據就建立一個 if (!this._sectionManager) { this._sectionManager = new SectionManager(this.sectionSize); } // 咱們須要去遍歷去註冊它,爲每個塊都設置一個對應的信息方便用於查找他 this.collection.forEach((item, index) => { // 註冊塊 -> 爲每個塊都設置一個對應的信息 this._sectionManager.registerCell({ index, cellMetadatum: this.cellSizeAndPositionGetter(item, index) }); }); }, // 咱們須要一個方法去計算這些塊的信息 -> 用於計算每個塊顯示的大小和顯示的位置 cellSizeAndPositionGetter (item, index) { // 計算大小和位置 return { width: 100, height: 150, x: (index % 2 * 110), y: parseInt(index / 2) * 160 } } } watch: { // 監聽數據的變化從新從新註冊塊進行渲染 collection() { this.registerCellsToSectionManager() } } </script>
複製代碼

上面中咱們已經建立了全部塊的所應該包含的信息了,接下來咱們應該去建立全部塊的總高度 = 建立滾動區域

App.vue
<template>
  <div class="vue-wrapper" :style="wrapperStyle">
    <div class="vue-wrapper-container" :style="scrollHeight">
      <div class="cell-container" v-for="(item, index) in displayItems">
        {{item.data}}
      </div>
    </div>
  </div>
</template>

<script> computed: { // 建立滾動區域 scrollHeight () { let scrollHeight = 0; let scrollWidth = 0; // 遍歷循環計算出滾動區域的總寬度和總高度 this._sectionManager._cellMetadata.forEach((sizeAndPosition) => { const {x, y, width, height} = sizeAndPosition; const bottom = y - height; const right = x - width; if (bottom > scrollHeight) { scrollHeight = bottom } if (right > scrollWidth) { scrollWidth = right } }); return { height: scrollHeight + 'px', width: scrollWidth + 'px' } } } </script>
複製代碼

有了滾動區域後咱們應該去建立當前視圖中所應該渲染的塊是那些

App.vue
<template>
  <div class="vue-wrapper" :style="wrapperStyle" ref="VueWrapper">
    <div class="vue-wrapper-container" :style="scrollHeight">
      <div class="cell-container" v-for="(item, index) in displayItems" :style="getComputedStyle(item, index)">
        {{item.data}}
      </div>
    </div>
  </div>
</template>
<script> // 設置當前視圖咱們中應該顯示那些塊 flushDisplayItems () { let scrollTop = 0; let scrollLeft = 0; // 設置能夠滾動的高度和寬度 if (this.$refs.VueWrapper) { scrollTop = this.$refs.VueWrapper.scrollTop; scrollLeft = this.$refs.VueWrapper.scrollLeft; } // 而後這裏咱們須要去設置當前視圖中應該渲染那些塊 // 因而咱們要在 SectionManager類中定義一個方法去獲取須要渲染的那個塊的索引 let index = this._sectionManager.getCellIndex({ height: this.height, width: this.width, x: scrollLeft, y: scrollTop }); // 到這裏咱們已經獲取到了索引了,而後咱們就能夠去渲染該視圖所對應的塊了 const displayItems = []; index.forEach((index) => { displayItems.push({ index, ...this.collection[index] }); }); if (window.requestAnimationFrame) { window.requestAnimationFrame(() => { this.displayItems = displayItems; // 強制更新當前組件(以及 Slot 裏面的組件,但不包含所有子組件 ) this.$forceUpdate(); }) } else { this.displayItems = displayItems; // 強制更新當前組件(以及 Slot 裏面的組件,但不包含所有子組件 ) this.$forceUpdate(); } }, // 獲取到視圖應該渲染那些塊以外咱們還須要設置這些塊所應該在的位置 getComputedStyle(displayItem) { if (!displayItem) { return; } const { width, height, x, y } = this._sectionManager._cellMetadata[displayItem.index]; return { left: `${x}px`, top: `${y}px`, width: `${width}px`, height: `${height}px` } watch: { // 監聽數據的變化從新從新註冊塊進行渲染 collection() { this._sectionManager = new SectionManager(this.sectionSize) this.registerCellsToSectionManager(); } } } </script>
複製代碼
SectionManager.js
// 獲取須要渲染那些塊的索引
// 一個塊中可能會包含其餘塊的部分範圍
getCellIndex ({height, width, x, y}) {
const indices = {};

this.getSections({height, width, x, y}).forEach((section) => {
  // 獲取全部塊的索引
  section.getCellIndex().forEach((index) => {
    indices[index] = index
  });
})

// 由於indices是一個Object因此咱們要把它轉換成Number來獲得索引
return Object.keys(indices).map((index) => {
  return indices[index];
    });
}
複製代碼

渲染完後咱們已經獲得了當前視圖中應該顯示那些塊了,而後最後一步就是須要定義一個滾動方法去再次渲染當前滾動區域應該顯示那些塊

<template>
  <div class="vue-wrapper" :style="wrapperStyle" @scroll.passive="onScroll" ref="VueWrapper">
    <div class="vue-wrapper-container" :style="scrollHeight">
      <div class="cell-container" v-for="(item, index) in displayItems" :style="getComputedStyle(item, index)">
        {{item.data}}
      </div>
    </div>
  </div>
</template>
<script> onScroll(e) { this.flushDisplayItems(); } </script>
複製代碼

到這裏咱們已經完成了這個組件的製做了。 咱們不由能夠感嘆「塊渲染」的思想是如此的精妙啊~~~~~~~~~,這個也是 react-virtualize的核心思想。

最後獻上源碼分析代碼

若有不正確,歡迎任何形式的PR、Issue ~

本文參考:

vue-virtual-collection

相關文章
相關標籤/搜索