手把手教你实现一个简单高效的虚拟列表组件

1,890 阅读3分钟

在做项目的过程中不可避免的会有展示列表数据的功能,但是当数据量过大时就会出现滚动卡顿的问题,这是因为列表中的每一项都会生成对应的dom结构,有没有什么更好的方法呢?这就是虚拟列表要解决的问题。

什么是虚拟列表

20240524_153547-ezgif.com-video-to-gif-converter.gif

一图胜千言, 从图中可以看出,dom结构是固定不变的,不断改变的只是dom结构中的内容,这就是虚拟列表最终的效果。

虚拟列表实现

首先创建一个virtualList的组件然后在页面中引入,然后给组件传入数据源和默认要显示的数量。

<template>
  <div id="app">
    <VirtualList :list="list" :showNum="10"/>
  </div>
</template>

<script>
export default {
    components:{
        VirtualList
    },
    computed:{
        list(){
            return new Arrya(1000).fill('').map((item,index)=>{
                return {
                    id:index+1,
                    content:`我是内容${index}`
                }
            })
        }
    },
}
</script>

在组件中首先写下模板结构,然后在组件中接受父组件传递过来的数据

<template>
        <div class="list-container" ref="listContainer">
            <ul class="list" ref="list">
                <li v-for="item in list" :key="item.id">{{ item.content }}</li>
            </ul>
        </div>
</template>

<script>
export default {
   data(){
       return {
           start:0, // 开始下标
           end:10, // 结束下标
           itemHeight:30 // 列表中每一项的高度
       }
   },
   props: {
        list: {
            type: Array,
        },
        showNum: {
            type: Number,
        }
    },
}
</script>

<style>
.list-container {
    width:600px;
    position: relative;
    overflow-y: scroll;
    margin: 0px auto;
}
.list{
  list-style:none;
  margin:0
}
.list li{
    height: 30px;
    line-height: 30px;
    text-align: center;
}
</style>

可以看到这里并没有给最外层容器高度,是因为这个高度是需要计算出来的,计算方法也很简单,容器整体的高度应该是让每一项item的高度乘上要显示的数量。

mounted(){
    this.$refs.listContainer.style.height = this.itemHeight * this.showNum + 'px';
}

现在就确定了整个容器的高度,当内容超出了容器的整体高度是就需要向下滚动,所以需要对滚动事件进行监听。

<template>
        <div class="list-container" ref="listContainer" @scroll="handleScroll">
            <ul class="list" ref="list">
                <li v-for="item in showList" :key="item.id">{{ item.value }}</li>
            </ul>
        </div>
</template>

handleScroll事件中应该做什么呢?这里其实是核心部分,主要思路是根据计算出来的区间对源数组进行分割,这时候就需要用到之前定义的startend下标了。首先我们需要知道用户滚动了多少距离,可以使用scrollTop属性来获取滚动条距离顶部的距离,然后再把这个滚动的距离跟item的高度对比看滚过去了多少个item,然后就把这个值作为start,有了startend就可以根据显示的数量计算出结束的位置。

handleScroll(){
    const scrollTop = this.$refs.listContainer.scrollTop;
    this.start = ~~(scrollTop / this.itemHeight); // ~~运算符用于取整
    this.end = this.start + this.showNum;
}

有了开始位置和结束位置就可以对数组中的元素进行选取,这里比较适合使用计算属性。

<template>
// ...
<li v-for="item in showList" :key="item.id">{{ item.content }}</li>
</template>

<script>
export default {
    computed:{
        // ...
        showList(){
            return this.list.slice(this.start,this.end)
        }
    },
}
<script>

原来模板中的数组也要替换成计算后的数组。那么此时页面中就默认显示区间内的元素了,那如何实现元素动态更新呢,其实只要动态更改开始和结束位置就可以了。

现在又出现了一个新的问题,就是现在默认展示比如说是10条数据,数据是展示出来了但是页面没有办法向下滚动也就是说没有滚动条了。那我们可以模拟一个滚动条出来。

 <div class="list-container" ref="listContainer" @scroll="handleScroll">
   // 模拟滚动条的div
   <div class="scroll-bar" ref="scrollBar"></div>
     <ul class="list" ref="list">
        <li v-for="item in showList" :key="item.id">{{ item.value }}</li>
      </ul>
    </div>
 </div>
 
 mounted(){
     // ...
     this.$refs.scrollBar.style.height = this.itemHeight * this.list.length + 'px';
 }

通过创建一个空的div并且在初始化时使用总列表的长度乘上每一个列表项的高度同时计算出滚动条的高度,这样一来无法滚动的问题就已经解决了。

此时页面上应该什么内容也没有显示,因为滚动条的高度跟很高已经把列表顶到最下面去了,所以需要给列表添加绝对定位。

.list{
  position:absolute;
  top: 0;
  left: 0;
  list-style:none;
  margin:0;
}

此时元素就重新显示出来了,向下滚动时整个列表会被滚上去,那么剩下的最后一个步骤就是滚动的同时需要不断地去修改列表整体的top值。

那这个值应该怎么计算呢?它其实是由开始下标和每一项的高度决定的,试想滚上去的高度不就是列表距离顶部的距离吗(这里思考五分钟)那么滚上去的高度怎么计算呢?其实就是让开始的下标乘上每一项的高度,但是有的同学会说直接用scrollTop多省事啊,这种方法也可以,只是效果体验上不如另一种方式好,感兴趣的同学可以自行测试哈。

handleScroll(){
    // ...
    this.$refs.list.style.top = this.start * this.itemHeight + 'px';
}

总结

以上就是整个虚拟列表的简单实现了,同时也解决了滚动卡顿的问题,代码并没多少思路却很重要,所以还是需要多思考。

完整代码