前端: 如何优化列表大批量的数据渲染

需求点:如何列表数据渲染进行优化?

最近业务上也碰到这个问题点。上网也查了查资料,貌似也经常问,特此写文章记录下来。

关于如何处理以上上面的业务痛点:
就两点:
1 、虚拟列表是最主流的解决方案,不渲染所有的数据,只渲染可视区域中的数据。当用户滑(滚)动时,通过监听 scroll 来判断是上滑还是下拉,从而更新数据。同理 IntersectionObserver 和 getBoundingClientRect 都能实现
2、时间分片主要是分批渲染DOM,使用 requestAnimationFrame 来让动画更加流畅
tips: 第二点就不详细讲述了,文末有链接对应,可自行享用。

话不多说直接上代码:不懂的可下方评论或者私信:

<template>
  <div
    ref="list"
    :style="{height}"
    class="infinite-list-container"
    @scroll="scroll($event)"
  >
    <div
      ref="phantom"
      class="infinite-list-phantom"
    />
    <div
      ref="content"
      class="infinite-list"
    >
      <div>
        <div
          v-for="item in visibleData"
          ref="items"
          :id="item._index"
          :key="item._index"
          class="infinite-list-item"
        >
          <!-- <slot ref="slot" :item="item.item"></slot> -->
          <p>
            <span style="color:red">{{ item.id }}</span>
            {{ item.value }}
          </p>
        </div>
      </div>
    </div>
  </div>
</template>
<script>
export default {
  props: {
    /** 需要渲染的数据 */
    listData: {
      type: Array,
      default: () => []
    },
    /** 每项高度 */
    itemSize: {
      type: Number,
      default: 200
    },
    /** 视口高度 */
    height: {
      type: String,
      default: '100%'
    }
  },
  data() {
    return {
      screenHeight: 0, // 可视区域高度
      startOffset: 0, // 偏移量
      start: 0, // 起始索引
      end: null // 结束索引
    };
  },
  computed: {
    /** 列表总高度 */
    listHeight() {
      return this.listData.length * this.itemSize;
    },

    /** 可显示的列表项数 */
    visibleCount() {
      return Math.ceil(this.screenHeight / this.itemSize);
    },

    /** 偏移量对应的style */
    getTransform() {
      return `transform3d(0, ${this.startOffset}px, 0)`;
    },

    /** 可显示列表数据 */
    visibleData() {
      return this.listData.slice(this.start, Math.min(this.end, this.listData.length));
    }
  },
  mounted() {
    this.screenHeight = this.$el.clientHeight;
    this.start = 0;
    this.end = this.start + this.visibleCount;
  },
  created() {
    this.initPositions();
  },
  updated() {
    this.$nextTick(function() {
      if (!this.$refs.items || !this.$refs.items.length) {
        return;
      }
      // 获取真实元素大小,修改对应的尺寸缓存
      this.updateItemsSize();
      // 更新列表总高度
      let height = this.positions[this.positions.length - 1].bottom;
      this.$refs.phantom.style.height = height + 'px';
      // 更新真实偏移量
      this.setStartOffset();
    });
  },
  methods: {
    /** 滚动事件 */
    scroll(e) {
      // 当前滚动位置
      let scrollTop = this.$refs.list.scrollTop;
      // 此时的开始索引
      this.start = this.getStartIndex(scrollTop);
      // 此时的结束索引
      this.end = this.start + this.visibleCount;
      // 此时的偏移量
      this.setStartOffset();
    },

    initPositions() {
      this.positions = this.listData.map((d, index) => ({
        index,
        height: this.estimatedItemSize,
        top: index * this.estimatedItemSize,
        bottom: (index + 1) * this.estimatedItemSize
      }));
    },

    /** 获取列表起始索引 */
    getStartIndex(scrollTop = 0) {
      return this.binarySearch(this.positions, scrollTop);
    },
    /** 二分查找 */
    binarySearch(list, value) {
      let start = 0;
      let end = list.length - 1;
      let tempIndex = null;
      while (start <= end) {
        let midIndex = parseInt((start + end) / 2);
        let midValue = list[midIndex].bottom;
        if (midValue === value) {
          return midIndex + 1;
        } else if (midValue < value) {
          start = midIndex + 1;
        } else if (midValue > value) {
          if (tempIndex === null || tempIndex > midIndex) {
            tempIndex = midIndex;
          }
          end = end - 1;
        }
      }
      return tempIndex;
    },
    /** 获取当前列表项的当前尺寸 */
    updateItemsSize() {
      let nodes = this.$refs.items;
      nodes.forEach(node => {
        let rect = node.getBoundingClientRect();
        let height = rect.height;
        let index = +node.id.slice(1);
        let oldHeight = this.positions[index].height;
        let dValue = oldHeight - height;
        // 存在差值
        if (dValue) {
          this.positions[index].bottom = this.positions[index].bottom - dValue;
          this.positions[index].height = height;

          for (let k = index + 1; k < this.positions.length; k++) {
            this.positions[k].top = this.positions[k - 1].bottom;
            this.positions[k].bottom = this.positions[k].bottom - dValue;
          }
        }
      });
    },
    /** 获取当前位置的便宜量 */
    setStartOffset() {
      let startOffset = this.start >= 1 ? this.positions[this.start - 1].bottom : 0;
      this.$refs.content.style.transform = `translate3d(0,${startOffset}px,0)`;
    }
  }
};
</script>

<style lang="less" scoped>
.infinite-list-container {
	overflow: auto;
	position: relative;
	-webkit-overflow-scrolling: touch;
}

.infinite-list-phantom {
	position: absolute;
	left: 0;
	top: 0;
	right: 0;
	z-index: -1;
}

.infinite-list {
	left: 0;
	right: 0;
	top: 0;
	position: absolute;
}

.infinite-list-item {
	padding: 5px;
	color: #555;
	box-sizing: border-box;
	border-bottom: 1px solid #999;
}
</style>

这几篇文章讲述的很详细还有对应效果:
link
link2

你可能感兴趣的:(vue,前端,javascript,vue.js)