前言

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

欧阳也在找工作,坐标成都求内推!

什么是虚拟列表

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

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

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

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

实现滚动条

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

<template>
<div class="container">
<div class="list-wrapper">
<!-- 只渲染可视区域列表数据 -->
</div>
</div>
</template> <style scoped>
.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,这样可视区域就被撑开了,并且滚动条也有了。代码如下:

<template>
<div class="container">
<div class="placeholder" :style="{ height: listHeight + 'px' }"></div>
<div class="list-wrapper">
<!-- 只渲染可视区域列表数据 -->
</div>
</div>
</template> <script setup>
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 {
height: 100%;
overflow: auto;
position: relative;
}
.placeholder {
position: absolute;
left: 0;
top: 0;
right: 0;
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 = 0renderCount = 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重新回到可视范围内。

这个是运行效果图:

下面是完整的代码:

<template>
<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"
:key="item.id"
:style="{
height: itemSize + 'px',
lineHeight: itemSize + 'px',
backgroundColor: `rgba(0,0,0,${item.value / 100})`,
}"
>
{{ item.value + 1 }}
</div>
</div>
</div>
</template> <script setup>
import { ref, onMounted, computed } from "vue";
const { listData, itemSize } = defineProps({
listData: {
type: Array,
default: () => [],
},
itemSize: {
type: Number,
default: 100,
},
}); const container = ref(null);
const containerHeight = ref(0);
const renderCount = computed(() => Math.ceil(containerHeight.value / itemSize));
const start = ref(0);
const offset = ref(0);
const end = computed(() => start.value + renderCount.value);
const listHeight = computed(() => listData.length * itemSize);
const renderList = computed(() => listData.slice(start.value, end.value + 1)); const getTransform = computed(() => `translate3d(0,${offset.value}px,0)`); onMounted(() => {
containerHeight.value = container.value.clientHeight;
}); function handleScroll(e) {
const scrollTop = e.target.scrollTop;
start.value = Math.floor(scrollTop / itemSize);
offset.value = scrollTop - (scrollTop % itemSize);
}
</script> <style scoped>
.container {
height: 100%;
overflow: auto;
position: relative;
} .placeholder {
position: absolute;
left: 0;
top: 0;
right: 0;
z-index: -1;
} .card-item {
padding: 10px;
color: #777;
box-sizing: border-box;
border-bottom: 1px solid #e1e1e1;
}
</style>

这个是父组件的代码:

<template>
<div style="height: 100vh; width: 100vw">
<VirtualList :listData="data" :itemSize="100" />
</div>
</template> <script setup>
import VirtualList from "./common.vue";
import { ref } from "vue"; const data = ref([]);
for (let i = 0; i < 1000; i++) {
data.value.push({ id: i, value: i });
}
</script> <style>
html {
height: 100%;
}
body {
height: 100%;
margin: 0;
}
#app {
height: 100%;
}
</style>

总结

这篇文章我们讲了如何实现一个定高的虚拟列表,首先根据可视区域的高度和item的高度计算出视口内可以渲染出来的item数量renderCount。然后根据滚动的距离去计算start的位置,计算end的位置时使用start + renderCount 预渲染一个item。在每个item范围内滚动时直接复用浏览器的滚动,此时无需进行任何处理。当从一个item滚动到另外一个item时,此时会做两件事情:更新start的值和根据scrollTop计算列表的偏移值让新的start对应的item重新回到可视范围内。

关注公众号:【前端欧阳】,给自己一个进阶vue的机会

另外欧阳写了一本开源电子书vue3编译原理揭秘,看完这本书可以让你对vue编译的认知有质的提升。这本书初、中级前端能看懂,完全免费,只求一个star。

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

  1. 史上最简单的vi教程,10分钟包教会

    从第一次接触vi/vim到现在已经十几年了,在这个过程中,来来回回,反反复复,学习vi很多次了. 虽然关于vi的使用,我还远未达到"专家"的水平,但对于vi的使用,我有话说. 1. ...

  2. ios同步线程(dispatch_sync)保证代码在主线程中执行

    - (BOOL)transitionToNextPhase { // 保证代码在主线程 if (![[NSThread currentThread] isMainThread]) { dispatch ...

  3. 10分钟教你用python 30行代码搞定简单手写识别!

    欲直接下载代码文件,关注我们的公众号哦!查看历史消息即可! 手写笔记还是电子笔记好呢? 毕业季刚结束,眼瞅着2018级小萌新马上就要来了,老腊肉小编为了咱学弟学妹们的学习,绞尽脑汁准备编一套大学秘籍, ...

  4. 读入一个自然数n,计算其各位数字之和,用汉语拼音写出和的每一位数字。 输入格式:每个测试输入包含1个测试用例,即给出自然数n的值。这里保证n小于10的100次幂。 输出格式:在一行内输出n的各位数字之和的每一位,拼音数字间有1 空格,但一行中最后一个拼音数字后没有空格。 输入样例: 1234567890987654321123456789 输出样例: yi san wu

    这是PAT中的一道练习题 刚开始的时候我想着直接定义正整数n,结果走了很大的弯路,因为题目中要求n小于10的100次幂,即最大的正整数n有100位,而C语言中整型数字最大占8个字节的存储空间,如果按无 ...

  5. 五分钟学习React(二):我的第一个Hello World

    我的第一个React应用 接着我们上一期所讲的内容,通过create-react-app脚手架创建的应用,它是基于ES6的语法生成的.我们清空src目录下的文件,并分别创建index.js和index ...

  6. 10分钟了解 代理模式与java中的动态代理

    前言    代理模式又分为静态代理与动态代理,其中动态代理是Java各大框架中运用的最为广泛的一种模式之一,下面就用简单的例子来说明静态代理与动态代理. 场景    李雷是一个唱片公司的大老板,很忙, ...

  7. 现在有一个城市销售经理,需要从公司出发,去拜访市内的商家,已知他的位置以及商家的位置,但是由于城市道路交通的原因,他只能在左右中选择一个方向,在上下中选择一个方向,现在问他有多少种方案到达商家地址。给定一个地图map及它的长宽n和m,其中1代表经理位置,2代表商家位置,-1代表不能经过的地区,0代表可以经过的地区,请返回方案数,保证一定存在合法路径。保证矩阵的长宽都小于等于10。

    include "stdafx.h" #include<iostream> #include<vector> #include<algorithm&g ...

  8. 三分钟学会Redis在.NET Core中做缓存中间件

    大家好,今天给大家说明如何在.NET Core中使用Redis,我们在想要辩论程序的好与坏,都想需要一个可视化工具,我经常使用的是一位国内大牛开发的免费工具,其Github地址为: https://g ...

  9. 3分钟搞定高逼格PPT封底——简约型

    封底想要高逼格又简约? 发现了这五类,看完不会制作算我输.   一.纯文字 白色背景下,一段结束语,或提问或感谢.       重叠文字,看上去非常有创意. 没有操作难度,END放大字号,颜色设置为浅 ...

  10. 一篇文章教会你使用Java8中的Lambda表达式

    简介 Java 8为开发者带来了许多重量级的新特性,包括Lambda表达式,流式数据处理,新的Optional类,新的日期和时间API等.这些新特性给Java开发者带来了福音,特别是Lambda表达式 ...

随机推荐

  1. Kubernetes DaemonSet 控制器(二十二)

    通过该控制器的名称我们可以看出它的用法:Daemon,就是用来部署守护进程的,DaemonSet用于在每个 Kubernetes 节点中将守护进程的副本作为后台进程运行,说白了就是在每个节点部署一个 ...

  2. Windows系统无法打开‘’网络发现‘’功能

    Windows10无法开启网络发现 解决办法: 1. services.msc 2. 开启 SSDP Discovery ,设置 启动类型为 自动 ,服务状态为 启动 Windows7 无法开启网络发 ...

  3. Windows应急响应-Auto病毒

    目录 应急背景 分析样本 开启监控 感染病毒 查看监控 分析病毒行为 autorun.inf分析 2.异常连接 3.进程排查 4.启动项排查 查杀 1.先删掉autorun.inf文件 2.使用xue ...

  4. break,continue,return的只要区别

    a)break 结束当前循环体 b)continue 结束本次的循环,执行下次的循环 c)return 结束函数体,并返回值 d)break 和 continue 写在循环里(for,while)re ...

  5. 云原生周刊:K8s 的 13 个最佳开源监控工具

    开源项目推荐 Silver Surfer 该项目旨在检查 Kubernetes 对象的 API 版本兼容性,并为其提供迁移路径,以便为集群升级做好准备. Actions Runner Controll ...

  6. Vulhub 安装运行

    前言 vulhub是利用docker技术做的一个漏洞复现平台,可以一键搭建对应的配置.在下载好对应的代码包后,不需要安装,只需要解压并利用3条命令,就可以简单的创建关闭对应漏洞环境.最好是购买一台阿里 ...

  7. Flink Time

    基础概念 支持三种时间概念: Processing Time 时间递增 Ingestion Time : 摄入时间,数据进入Flink框架的时间,在Source Operator中设置,每个事件拿到当 ...

  8. linux开启快捷键,牛逼的狠~

    vim env_show #!/bin/bash echo '-----------A------------- XXXXXXXXXXXXXXXXXXXXXXXXXXX XXXXXXXXXXXXXXX ...

  9. 如何使用Flask编写一个网站

    使用Flask编写一个网站是一个相对简单且有趣的过程.Flask是一个用Python编写的轻量级Web应用框架.它易于上手,同时也非常强大,适合构建从简单的博客到复杂的Web应用的各种项目.以下是一个 ...

  10. Java8之新特性

    简单例子 实例: ​ 处理集合时,通常会迭代遍历它的元素,并在每个元素上执行某项操作,例如假设我们想要对某本的所有长单词进行计数,首先需要将所有单词放到一个列表中 var contents = new ...