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

相关文章
相关标签/搜索