浅谈一下列表优化

前言

在项目中若是能分页实现那最好不过了,不过不少时候长列表不可避免,这里又分两种状况javascript

  1. 第一次不用所有加载完成,这种可使用懒加载或者说无限滚动的方式来实现
  2. 另一种则是一次要渲染所有数据出现

下面就来讨论这两种状况如何进行优化,能够对比列表优化具体实现的源码来看 (注:下面是用 Vue 实现的,使用其余框架并不影响)html

无限滚动

实现的思路很简单就是根据滚动条是否滚动到底部(总高度 - 可见高度 - 滚动条高度),滚动到底部就添加新的数据java

function scroll({ target }) {
  const DISTANCE = 40;
  const h = target.scrollHeight - (target.clientHeight + target.scrollTop);
  if (h < DISTANCE) {
    for (let i = 0, j = this.list.length, l = this.list.length; i < l; i++) {
      this.list.push(j + i);
    }
  }
}
复制代码

虚拟列表

引用一张图,能够看见咱们实现的思路就是只渲染可见部分的列表,每次滚动条变化的时候更改展现的列表,在下面的演示中,咱们都会用到一个基础的 html 结构,这里先贴一下

<div class="root">
  <div class="container"></div>
  <ul class="content">
    <li class="item" v-for="item of nowList" :key="item.value">
      {{ item.value }}
    </li>
  </ul>
</div>
复制代码
.root {
  border: 1px solid #999;
  list-style: none;
  overflow: auto;
  height: 400px;
  position: relative;
  .container {
    position: absolute;
    left: 0;
    top: 0;
    right: 0;
    z-index: -1;
  }
  .content {
    .container();
    z-index: 1;
    margin: 0;
    padding: 0;
    list-style: none;
  }
  .item {
    border-bottom: 1px solid #ccc;
    padding-left: 40px;
  }
}
复制代码

上面结构作了两件事情git

  1. 固定总列表的高度,让其出现滚动条
  2. 用一个遮罩 div 撑起整个列表的高度

固定

这里假设每一个列表的高度为 30px,剩下的部分就是计算出列表的整体高度以及开始索引结束索引,核心代码只有不到 10 行github

scroll() {
  const dom = this.$refs.root;
  const total = Math.ceil(dom.clientHeight / this.height);
  const start = Math.floor(dom.scrollTop / this.height);
  const end = start + total;
  this.start = start;
  this.end = end;
}
复制代码

总索引: 当前视图的高度 / 子项的高度,不过注意须要向上取整; 开始索引: 滚动的距离 / 子项的高度 结束索: 总索引 + 开始索引 下面是完整的代码缓存

<template>
  <div>
    <div class="root" ref="root" @scroll="scroll">
      <div class="container" :style="{ height: totalHeight }"></div>
      <ul class="content" :style="{ transform: getTransform }">
        <li class="item" :style="{ height: height + 'px', lineHeight: height + 'px' }" v-for="(item, i) of nowList" :key="i" >
          {{ item }}
        </li>
      </ul>
    </div>
  </div>
</template>

<script> export default { data() { return { list: Array(10000) .fill(1) .map((f, i) => i), height: 30, start: 0, end: 0 }; }, computed: { totalHeight() { return this.height * this.list.length + "px"; }, nowList() { return this.list.slice(this.start, this.end); }, getTransform() { return `translate3d(0,${this.start * this.height}px,0)`; } }, mounted() { this.scroll(); }, methods: { scroll() { const dom = this.$refs.root; const total = Math.ceil(dom.clientHeight / this.height); const start = Math.floor(dom.scrollTop / this.height); const end = start + total; this.start = start; this.end = end; } } }; </script>

<style lang="less" scoped> .root { border: 1px solid #999; list-style: none; overflow: auto; height: 400px; position: relative; .container { position: absolute; left: 0; top: 0; right: 0; z-index: -1; } .content { .container(); z-index: 1; margin: 0; padding: 0; list-style: none; } .item { border-bottom: 1px solid #ccc; padding-left: 40px; } } </style>
复制代码

非固定

非固定须要考虑的更多则是性能的问题,下面先贴一个完整的代码,在须要说明部分已经注释了框架

<template>
  <div>
    <div class="root" ref="root" @scroll="scroll">
      <div class="container" :style="{ height: totalHeight }"></div>
      <ul class="content" :style="{ transform: getTransform }">
        <li class="item" v-for="item of nowList" :style="{ height: item.height + 'px', lineHeight: item.height + 'px' }" :key="item.value" >
          {{ item.value }}
        </li>
      </ul>
    </div>
  </div>
</template>

<script> export default { data() { return { list: Array(10000) .fill(1) .map((f, i) => { return { value: i, height: this.getRandom(10, 100) }; }), start: 0, end: 0, // 指针 pointer: -1, // 缓存 cache: {}, // 初始总数 initialHeight: 50 }; }, computed: { totalHeight() { // 这里是获取整体高度,判断了两种状况,第一种是给定初始总数,另一种则是没有,若是没有的话,高度就是已缓存的 + 未缓存的部分 if (this.initialtotal >= 0) { const { top, height } = this.pointer >= 0 ? this.getIndexOffset(this.pointer) : { top: 0, height: 0 }; return `${top + height + (this.list.length - 1 - this.pointer) * this.initialHeight}px`; } const { height } = this.list.reduce(function(x, y) { return { height: x.height + y.height }; }); return height + "px"; }, // 可视数据 nowList() { return this.list.slice( this.start, Math.min(this.end + 1, this.list.length) ); }, getTransform() { return `translate3d(0,${this.getIndexOffset(this.start).top}px,0)`; } }, mounted() { this.scroll(); }, methods: { // 滚动事件 scroll() { const dom = this.$refs.root; // 获取索引 const start = this.getIndex(dom.scrollTop); // 把当前可视的高度 + 滚动条的高度,再去取索引 const end = this.getIndex(dom.scrollTop + dom.clientHeight); this.start = start; this.end = end; }, // 取出指定范围随机数 getRandom(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; }, // 根据滚动条y获取指定坐标 getIndex(scrollTop) { // 判断思路很简单,若是高度大于滚动条确定就出现了,另一种则是判断了边界问题 let total = 0; for (let i = 0, j = this.list.length; i < j; i++) { if (total >= scrollTop || j - 1 === i) { return i; } // 这里主要是起缓存做用的 total += this.getIndexOffset(i).height; } return 0; }, // 获取指定坐标的位置和高度 getIndexOffset(index) { // 若是存在缓存中直接返回 if (this.pointer >= index) { return this.cache[index]; } let total = 0; // 这里是为了比较没有取到的状况 if (this.pointer >= 0) { const li = this.cache[this.pointer]; total = li.top + li.height; } // 注意上面由于取的值是li.top + li.height,因此i从 + 1开始 for (let i = this.pointer + 1; i <= index; i++) { const size = this.list[i].height; this.cache[i] = { top: total, height: size }; total += size; } if (index > this.pointer) { this.pointer = index; } return this.cache[index]; } } }; </script>

<style lang="less" scoped> .root { border: 1px solid #999; list-style: none; overflow: auto; height: 400px; position: relative; .container { position: absolute; left: 0; top: 0; right: 0; z-index: -1; } .content { .container(); z-index: 1; margin: 0; padding: 0; list-style: none; } .item { border-bottom: 1px solid #ccc; padding-left: 40px; } } </style>
复制代码

上面只最终的实现,实际上跟固定高度相比就是增长了获取索引的方法,固定高度咱们是知道对应子项的高度,因此能够经过可视高度来计算,而这里我用了随机数来设置高度,因此须要获取到对应的索引。 上面代码同时也作了两点优化,一是缓存,二是总高度优化less

总高度的实现有两种思路:dom

  1. 计算全部的高度,这种实际上有点浪费性能;
  2. 给定一个大概的值,拿缓存的值 + 没有缓存的值,没有缓存的值就是对应数据的长度 - 已缓存的坐标,以后每次缓存变化的时候再计算;

缓存则比较简单了,每次计算的时候把指针移动到计算的位置,同时将值添加上性能

参考文章

  1. zhuanlan.zhihu.com/p/34585166
相关文章
相关标签/搜索