Skip to content

本文由 简悦 SimpRead 转码, 原文地址 mp.weixin.qq.com

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

定高虚拟列表

在定高的虚拟列表中,我们可以根据可视区域的高度和每个 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>

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

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

- END -

如果您关注前端 + AI 相关领域可以扫码进群交流

添加小编微信进群😊

关于奇舞团

奇舞团是 360 集团最大的大前端团队,非常重视人才培养,有工程师、讲师、翻译官、业务接口人、团队 Leader 等多种发展方向供员工选择,并辅以提供相应的技术力、专业力、通用力、领导力等培训课程。奇舞团以开放和求贤的心态欢迎各种优秀人才关注和加入奇舞团。