专栏名称: 前端从进阶到入院
我是 ssh,只想用最简单的方式把原理讲明白。wx:sshsunlight,分享前端的前沿趋势和一些有趣的事情。
目录
相关文章推荐
四川大学本科招生  ·  寒假,川大的你,在哪里,有什么故事? ·  昨天  
四川大学本科招生  ·  寒假,川大的你,在哪里,有什么故事? ·  昨天  
兰州大学萃英在线  ·  年味变淡?我们何去何从 ·  2 天前  
浙江大学  ·  大换装!跟浙大一起迎接春天! ·  3 天前  
武汉大学  ·  武大 bot!没上春晚扭秧歌,但和省长握手了 ·  2 天前  
兰州大学萃英在线  ·  一夜鱼龙舞 ·  4 天前  
51好读  ›  专栏  ›  前端从进阶到入院

给我 2 分钟,保证教会你在 Vue3 中实现一个定高的虚拟列表

前端从进阶到入院  · 公众号  ·  · 2024-12-19 14:18

正文

前言

虚拟列表对于大部分一线开发同学来说是一点都不陌生的东西了,有的同学是直接使用第三方组件。但是面试时如果你简历上面写了虚拟列表,却给面试官说是通过三方组件实现的,此时空气可能都凝固了。所以这篇文章欧阳将会教你2分钟内实现一个定高的虚拟列表,至于不定高的虚拟列表下一篇文章来写。

什么是虚拟列表

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

一个常见的虚拟列表是下面这样的,如下图:

其中实线框的item表示在视口区域内真实渲染DOM,虚线框的item表示并没有渲染的DOM。

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

实现滚动条

按照上面的图,很容易想到我们的dom结构应该是下面这样的:


  <div class="container">
    <div class="list-wrapper">
      
    div>
  div>

</template>


  .container {
    height: 100%;
    overflow: auto;
    position: relative;
  }
style>

给可视区域 container 设置高度 100% ,也可以是一个固定高度值。并且设置 overflow: auto; 让内容在可视区域中滚动。

此时我们遇见第一个问题,滚动条是怎么来的,可视区域是靠什么撑开的?

答案很简单,我们知道每个item的高度 itemSize ,并且知道有多少条数据 listData.length 。那么 itemSize * listData.length 不就是真实的列表高度了吗。所以我们可以在可视区域 container 中新建一个名为 placeholder 的空div,将他的高度设置为 itemSize * listData.length ,这样可视区域就被撑开了,并且滚动条也有了。代码如下:


  <div class="container">
    <div class="placeholder" :style="{ height: listHeight + 'px' }">div>
    <div class="list-wrapper">
      
    div>
  div>

</template>


  import { ref, onMounted, computed } from "vue";
  const { listData, itemSize } = defineProps({
    listData: {
      type: Array,
      default: () => [],
    },
    itemSize: {
      type: Number,
      default: 100,
    },
  });

  const listHeight = computed(() => listData.length * itemSize);
script>

<style scoped>
  .container {
    height100%;
    overflow: auto;
    position: relative;
  }
  .placeholder {
    position: absolute;
    left0;
    top0;
    right0;
    z-index: -1;
  }
style>

placeholder 采用绝对定位,为了不挡住可视区域内渲染的列表,所以将其设置为 z-index: -1

接下来就是计算容器里面到底渲染多少个item,很简单, Math.ceil(可视区域的高度 / 每个item的高度)

为什么使用 Math.ceil 向上取整呢?

只要有个item在可视区域漏了一点出来,我们也应该将其渲染。

此时我们就能得到几个变量:

  • start :可视区域内渲染的第一个item的index的值,初始化为0。

  • renderCount :可视区域内渲染的item数量。

  • end :可视区域内渲染的最后一个item的index值,他的值等于 start + renderCount 。注意我们这里使用 start + renderCount 实际是多渲染了一个item,比如 start = 0 renderCount = 2 ,我们设置的是 end = 2 ,实际是渲染了3个item。目的是为了预渲染下一个,后面会讲。

监听滚动事件

有了滚动条后就可以开始滚动了,我们监听 container 容器的scroll事件。

可视区域中的内容应该随着滚动条的滚动而变化,也就是说在scroll事件中我们需要重新计算 start 的值。

function handleScroll(e{
  const scrollTop = e.target.scrollTop;
  start.value = Math.floor(scrollTop / itemSize);
  offset.value = scrollTop - (scrollTop % itemSize);
}

如果当前 itemSize 的值为100。

如果此时滚动的距离在0-100之间,比如下面这样:

上面这张图item1还没完全滚出可视区域,有部分在可视区域内,部分在可视区域外。此时可视区域内显示的就是 item1-item7 的模块了,这就是为什么前面我们计算end时要多渲染一个item,不然这里item7就没法显示了。

滚动距离在0-100之间时,渲染的DOM没有变化,我们完全是复用浏览器的滚动,并没有进行任何处理。

scrollTop 的值为100时,也就是刚刚把item1滚到可视区外面时。此时item1已经不需要渲染了,因为已经看不见他了。所以此时的 start 的值就应该从 0 更新为 1 ,同理如果 scrollTop 的值为 110 ,start的值也一样是 1 。所以得出 start.value = Math.floor(scrollTop / itemSize); 如下图:

此时的 start 从item2开始渲染,但是由于前面我们复用了浏览器的滚动,所以实际渲染的DOM第一个已经在可视区外面了。此时可视区看见的第一个是item3,很明显是不对的,应该看见的是第一个是item2。

此时应该怎么办呢?

很简单,使用 translate 将列表向下偏移一个item的高度就行,也就是100px。列表偏移后就是下面这样的了:

如果当前 scrollTop 的值为200,那么偏移值就是200px。所以我们得出

offset.value = scrollTop - (scrollTop % itemSize);

为什么这里要减去 scrollTop % itemSize 呢?

因为在滚动时如果是在item的高度范围内滚动,我们是复用浏览器的滚动,此时无需进行偏移,所以计算偏移值时需要减去 scrollTop % itemSize

实际上从一个item滚动到另外一个item时,比如从 item0 滚动到 item1 。此时会做两件事情:将 start 的值从 0 更新为 1 和根据 scrollTop 计算得到列表的偏移值 100 ,从而让新的start对应的 item1 重新回到可视范围内。

这个是运行效果图:

下面是完整的代码:


  <div ref="container" class="container" @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 in renderList"






请到「今天看啥」查看全文