定高虚拟列表

128 阅读4分钟

有的特殊场景我们不能分页,只能渲染一个长列表。这个长列表中可能有几万条数据,如果全部渲染到页面上用户的设备差点可能就会直接卡死了,这时我们就需要虚拟列表来解决问题。

定高虚拟列表

在定高的虚拟列表中,我们可以根据可视区域的高度和每个 item 的高度计算得出在可视区域内可以渲染多少个 item。不在可视区域里面的 item 那么就不需要渲染了(不管有几万个还是几十万个 item),这样就能解决长列表性能很差的问题啦。

如何实现定高虚拟列表呢?

  • 如何实现滚动条
  • 确定可视区域内有多少元素
  • 确定列表的首位索引和末尾索引
  • 滚动的时候更新首位索引和末尾索引

如何实现滚动条

在 container 里加一个全列表高度的元素 placeholder,假设每个元素的高度是 100,滚动的容器的高度 list.length*item.height。其中 placeholder 采用绝对定位,为了不挡住可视区域内渲染的列表,所以将其设置为 z-index: -1

<template>
  <div class="content" ref="content">
    <div class="placeholder" :style="{ height: listHeight + 'px' }"></div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      listData: [
        1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3,
        4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7,
        8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3,
        4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7,
        8, 91, 2, 3, 4, 5, 6, 7, 8, 9,
      ],
      itemSize: 100,
    }
  },
  computed: {
    // 滚动条高度
    listHeight() {
      return this.listData.length * this.itemSize
    },
  },
  mounted() {},
  methods: {},
}
</script>

<style scoped lang="scss">
.content {
  height: 100vh;
  overflow: auto;
  position: relative;
}
.placeholder {
  position: absolute;
  left: 0;
  top: 0;
  right: 0;
  z-index: -1;
}
</style>

确定可视区域内有多少元素?

通过 Math.ceil(可视区域的高度 / 每个 item 的高度)可以计算容器里渲染多少个 item。为什么是 Math.ceil 呢?因为只要有一个元素漏出来一点点也是算一个元素。 那么就可以得到几个变量~

  1. start:首位索引,默认 0
  2. renderCount:可视区域内渲染的 item 数量。
  3. end: 末尾索引,start+renderCount
  4. renderList: 可视区域的列表
<template>
  <div class="content" ref="content">
    <div class="placeholder" :style="{ height: listHeight + 'px' }"></div>
    <!-- 只渲染可视区域列表数据 -->
    <div
      class="card-item"
      v-for="(item, i) in renderList"
      :key="i"
      :style="{
        height: itemSize + 'px',
        lineHeight: itemSize + 'px',
        backgroundColor: `rgba(0,0,0,${item / 100})`,
      }"
    >
      {{ item + 1 }}
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      listData: [
        1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3,
        4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7,
        8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3,
        4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7,
        8, 91, 2, 3, 4, 5, 6, 7, 8, 9,
      ],
      itemSize: 100,
      start: 0,
      containerHeight: 0,
    }
  },
  computed: {
    // 滚动条高度
    listHeight() {
      return this.listData.length * this.itemSize
    },
    // 可视区域的列表
    renderList () {
      return this.listData.slice(this.start, this.end + 1)
    },
    // 获取可视区域一共有多少个元素
    renderCount () {
      return  Math.ceil(this.containerHeight / this.itemSize)
    },
    end () {
      return this.start + this.renderCount
    },
  },
  mounted() {
    // 获取可视区域高度
    this.containerHeight = this.$refs.content.clientHeight;
  },
  methods: {},
}
</script>

<style scoped lang="scss">
.content {
  height: 100vh;
  overflow: auto;
  position: relative;
}
.placeholder {
  position: absolute;
  left: 0;
  top: 0;
  right: 0;
  z-index: -1;
}
</style>

接下来监听滚动事件就可以了 重新计算start值,Math.floor(scrollTop / itemSize),为什么是Math.floor?因为如果元素只是向上滚动了一些但是还没有完全滚动上去,state值是不能更新的

<template>
  <div class="content" ref="content" @scroll="handleScroll($event)">
    <div class="placeholder" :style="{ height: listHeight + 'px' }"></div>
    <!-- 只渲染可视区域列表数据 -->
    <div
      class="card-item"
      v-for="(item, i) in renderList"
      :key="i"
      :style="{
        height: itemSize + 'px',
        lineHeight: itemSize + 'px',
        backgroundColor: `rgba(0,0,0,${item / 100})`,
      }"
    >
      {{ item + 1 }}
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      listData: [
        1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3,
        4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7,
        8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3,
        4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7,
        8, 91, 2, 3, 4, 5, 6, 7, 8, 9,
      ],
      itemSize: 100,
      start: 0,
      containerHeight: 0,
    }
  },
  computed: {
    // 滚动条高度
    listHeight() {
      return this.listData.length * this.itemSize
    },
    // 可视区域的列表
    renderList () {
      return this.listData.slice(this.start, this.end + 1)
    },
    // 获取可视区域一共有多少个元素
    renderCount () {
      return  Math.ceil(this.containerHeight / this.itemSize)
    },
    end () {
      return this.start + this.renderCount
    },
  },
  mounted() {
    // 获取可视区域高度
    this.containerHeight = this.$refs.content.clientHeight;
  },
  methods: {
    handleScroll (e) {
      const scrollTop = e.target.scrollTop;
      this.start = Math.floor(scrollTop / this.itemSize);
      this.offset = scrollTop - (scrollTop % this.itemSize);
    }
  },
}
</script>

<style scoped lang="scss">
.content {
  height: 100vh;
  overflow: auto;
  position: relative;
}
.placeholder {
  position: absolute;
  left: 0;
  top: 0;
  right: 0;
  z-index: -1;
}
</style>

到了这里,一个初步的虚拟列表就已经完成了,但是这时会出现一个,滑动的时候会多滚动上去一个元素~为什么会出现这个问题?!

上面步骤中我们用了浏览器的滚动事件更新start,更新到准确的start时,浏览器已经滚动上去一个元素的高度了。也就是我们需要把列表向下偏移一个 item 的高度就行

完整代码

<template>
  <div class="content" ref="content" @scroll="handleScroll($event)">
    <div class="placeholder" :style="{ height: listHeight + 'px' }"></div>
    <div class="list-wrapper" :style="{ transform: getTransform }">
      <!-- 只渲染可视区域列表数据 -->
      <div
        class="card-item"
        v-for="(item, i) in renderList"
        :key="i"
        :style="{
          height: itemSize + 'px',
          lineHeight: itemSize + 'px',
          backgroundColor: `rgba(0,0,0,${item / 100})`,
        }"
      >
        {{ item + 1 }}
      </div>
    </div>
  </div>
</template>

<script>
export default {
  data () {
    return {
      listData: [1,2,3,4,5,6,7,8,9,1,2,3,4,5,6,7,8,91,2,3,4,5,6,7,8,91,2,3,4,5,6,7,8,91,2,3,4,5,6,7,8,91,2,3,4,5,6,7,8,91,2,3,4,5,6,7,8,91,2,3,4,5,6,7,8,91,2,3,4,5,6,7,8,91,2,3,4,5,6,7,8,91,2,3,4,5,6,7,8,91,2,3,4,5,6,7,8,91,2,3,4,5,6,7,8,91,2,3,4,5,6,7,8,91,2,3,4,5,6,7,8,91,2,3,4,5,6,7,8,91,2,3,4,5,6,7,8,91,2,3,4,5,6,7,8,91,2,3,4,5,6,7,8,9],
      itemSize: 100,
      start: 0,
      containerHeight: 0,
      offset: 0,
    }
  },
  computed: {
    listHeight () {
      return this.listData.length * this.itemSize
    },
    renderList () {
      return this.listData.slice(this.start, this.end + 1)
    },
    // 获取可视区域一共有多少个元素
    renderCount () {
      return  Math.ceil(this.containerHeight / this.itemSize)
    },
    end () {
      return this.start + this.renderCount
    },
    getTransform () {
      return `translate3d(0,${this.offset}px,0)`
    }
  },
  mounted () {
    // 获取可视区域高度
    this.containerHeight = this.$refs.content.clientHeight;
  },
  methods: {
    handleScroll (e) {
      const scrollTop = e.target.scrollTop;
      this.start = Math.floor(scrollTop / this.itemSize);
      // 偏移量
      this.offset = scrollTop - (scrollTop % this.itemSize);
    }
  }
}

</script>

<style scoped lang="scss">
  .content {
    height: 100vh;
    overflow: auto;
    position: relative;
  }
  .placeholder {
    position: absolute;
    left: 0;
    top: 0;
    right: 0;
    z-index: -1;
  }
</style>

以上,一个定高的虚拟列表已经完成了

下期讲一下非定高虚拟列表