- VisualStudio2022插件的安装及使用-编程手把手系列文章
- pprof-在现网场景怎么用
- C#实现的下拉多选框,下拉多选树,多级节点
- 【学习笔记】基础数据结构:猫树
很多同学将虚拟列表当做亮点写在简历上面,但是却不知道如何手写,那么这个就不是加分项而是减分项了。在上一篇文章欧阳教会你 如何实现一个定高虚拟列表 ,但是实际项目中更多的是不定高虚拟列表,这篇文章欧阳来教你不定高如何实现。PS:建议先看看欧阳的上一篇 如何实现一个定高虚拟列表 后再来看这篇效果更佳.
欧阳也在找工作,坐标成都求内推! 。
不定高的意思很简单,就是不知道每一项item的具体高度,如下图:
现在我们有个问题,在不定高的情况下我们就不能根据当前滚动条的scrollTop去计算可视区域里面实际渲染的第一个item的index位置,也就是start的值.
没有start,那么就无法实现在滚动的时候只渲染可视区域的那几个item了.
既然我们不知道每个item的高度,那么就采用预估高度的方式去实现。比如这样:
const { listData, itemSize } = defineProps({
// 列表数据
listData: {
type: Array,
default: () => [],
},
// 预估item高度,不是真实item高度
itemSize: {
type: Number,
default: 300,
},
});
还是和上一篇一样的套路,计算出当前可视区域的高度containerHeight,然后结合预估的itemSize就可以得到当前可视区域里面渲染的item数量。代码如下:
const renderCount = computed(() => Math.ceil(containerHeight.value / itemSize));
注意:由于我们是预估的高度,所以这个renderCount的数量是不准的.
如果预估的高度比实际高太多,那么实际渲染的item数量就会不够,导致页面下方出现白屏的情况.
如果预估的高度太小,那么这里的item数量就会渲染的太多了,性能又没之前那么好.
所以预估item高度需要根据实际业务去给一个适当的值,理论上是宁可预估小点,也不预估的大了(大了会出现白屏).
start初始值为0,并且算出了renderCount,此时我们也就知道了可视区域渲染的最后一个end的值。如下:
const end = computed(() => start.value + renderCount.value);
和上一篇一样计算end时在下方多渲染了一个item,第一个item有一部分滚出可视区域的情况时,如果不多渲染可能就会出现白屏的情况.
有了start和end,那么就知道了可视区域渲染的renderList,代码如下:
const renderList = computed(() => listData.slice(start.value, end.value + 1));
这样我们就知道了,初始化时可视区域应该渲染哪些item了,但是因为我们之前是给每个item预估高度,所以我们应该将这些高度的值纠正过来.
为了记录不定高的list里面的每个item的高度,所以我们需要一个数组来存每个item的高度。所以我们需要定义一个positions数组来存这些值.
既然都存了每个item的高度,那么同样可以使用top、bottom这两个字段去记录每个item在列表中的开始位置和结束位置。注意bottom - top的值肯定等于height的值.
还有一个index字段记录每个item的index的值。positions定义如下:
const positions = ref<
{
index: number;
height: number;
top: number;
bottom: number;
}[]
>([]);
positions的初始化值为空数组,那么什么时候给这个数组赋值呢?
答案很简单,虚拟列表渲染的是props传入进来的listData。所以我们watch监听listData,加上immediate: true。这样就可以实现初始化时给positions赋值,代码如下:
watch(() => listData, initPosition, {
immediate: true,
});
function initPosition() {
positions.value = [];
listData.forEach((_item, index) => {
positions.value.push({
index,
height: itemSize,
top: index * itemSize,
bottom: (index + 1) * itemSize,
});
});
}
遍历listData结合预估的itemSize,我们就可以得出每一个item里面的height、top、bottom这几个字段的值.
还有一个问题,我们需要一个元素来撑开滚动条。在定高的虚拟列表中我们是通过itemSize * listData.length得到的。显然这里不能那样做了,由于positions数组中存的是所有item的位置,那么最后一个item的bottom的值就是列表的真实高度。前面也是不准的,会随着我们纠正positions中的值后他就是越来越准的了.
所以列表的真实高度为:
const listHeight = computed(
() => positions.value[positions.value.length - 1].bottom
);
此时positions数组中就已经记录了每个item的具体位置,虽然这个位置是错的。接下来我们就需要将这些错误的值纠正过来,如何纠正呢?
答案很简单,使用Vue的onUpdated钩子函数,这个钩子函数会在响应式状态变更而更新其 DOM 树之后调用。也就是会在renderList渲染成DOM后触发! 。
此时这些item已经渲染成了DOM节点,那么我们就可以遍历这些item的DOM节点拿到每个item的真实高度。都知道每个item的真实高度了,那么也就能够更新里面所有item的top和bottom了。代码如下:
<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.index"
ref="itemRefs"
:data-index="item.index"
>
<span style="color: red"
>{{ item.index }}
<img width="200" :src="item.imgUrl" alt="" />
</span>
{{ item.value }}
</div>
</div>
</div>
</template>
<script setup>
onUpdated(() => {
updatePosition();
});
function updatePosition() {
itemRefs.value.forEach((el) => {
const index = +el.getAttribute("data-index");
const realHeight = el.getBoundingClientRect().height;
let diffVal = positions.value[index].height - realHeight;
const curItem = positions.value[index];
if (diffVal !== 0) {
// 说明item的高度不等于预估值
curItem.height = realHeight;
curItem.bottom = curItem.bottom - diffVal;
for (let i = index + 1; i < positions.value.length - 1; i++) {
positions.value[i].top = positions.value[i].top - diffVal;
positions.value[i].bottom = positions.value[i].bottom - diffVal;
}
}
});
}
</script>
使用:data-index="item.index"将index绑定到item上面,更新时就可以通过+el.getAttribute("data-index")拿到对应item的index.
itemRefs中存的是所有item的DOM元素,遍历他就可以拿到每一个item,然后拿到每个item在长列表中的index和真实高度realHeight.
diffVal的值是预估的高度比实际的高度大多少,如果diffVal的值不等于0,说明预估的高度不准。此时就需要将当前item的高度height更新了,由于高度只会影响bottom的值,所以只需要更新当前item的height和bottom.
由于当前item的高度变了,假如diffVal的值为正值,说明我们预估的高度多了。此时我们需要从当前item的下一个元素开始遍历,直到遍历完整个长列表。我们预估多了,那么只需要将后面的所有item整体都向上移一移,移动的距离就是预估的差值diffVal.
所以这里需要从index + 1开始遍历,将遍历到的所有元素的top和bottom的值都减去diffVal.
将可视区域渲染的所有item都遍历一遍,将每个item的高度和位置都纠正过来,同时会将后面没有渲染到的item的top和bottom都纠正过来,这样就实现了高度的更新。理论上从头滚到尾,那么整个长列表里面的所有位置和高度都纠正完了.
通过前面我们已经实现了预估高度值的纠正,渲染过的item的高度和位置都是纠正过后的了。此时我们需要在滚动后如何计算出新的start的位置,以及offset偏移量的值.
还是和定高同样的套路,当滚动条在item中间滚动时复用浏览器的滚动条,从一个item滚到另外一个item时才需要更新start的值以及offset偏移量的值。如果你看不懂这句话,建议先看我上一篇如何实现一个定高虚拟列表 文章.
此时应该如何计算最新的start值呢?
很简单!在positions中存了两个字段分别是top和bottom,分别表示当前item的开始位置和结束位置。如果当前滚动条的scrollTop刚好在top和bottom之间,也就是scrollTop >= top && scrollTop < bottom,那么是不是就说明当前刚好滚到这个item的位置呢.
并且由于在positions数组中bottom的值是递增的,那么问题不就变成了查找第一个item的scrollTop < bottom。所以我们得出:
function getStart(scrollTop) {
return positions.value.findIndex((item) => scrollTop < item.bottom);
}
每次scroll滚动都会触发一次这个查找,那么我们可以优化上面的算法吗?
positions数组中的bottom字段是递增的,这很符合二分查找的规律。不了解二分查找的同学可以看看leetcode上面的这道题: https://leetcode.cn/problems/search-insert-position/description/.
所以上面的代码可以优化成这样:
function getStart(scrollTop) {
let left = 0;
let right = positions.value.length - 1;
while (left <= right) {
const mid = Math.floor((left + right) / 2);
if (positions.value[mid].bottom === scrollTop) {
return mid + 1;
} else if (positions.value[mid].bottom < scrollTop) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return left;
}
和定高的虚拟列表一样,当在start的item中滚动时直接复用浏览器的滚动,无需做任何事情。所以此时的offset偏移量就应该等于当前start的item的top值,也就是start的item前面的所有item加起来的高度。所以得出offset的值为:
offset.value = positions.value[start.value].top;
可能有的小伙伴会迷惑,在start的item中的滚动值为什么不算到offset偏移中去呢?
因为在start的item范围内滚动时都是直接使用的浏览器滚动,已经有了scrollTop,所以无需加到offset偏移中去.
所以我们得出当scroll事件触发时代码如下:
function handleScroll(e) {
const scrollTop = e.target.scrollTop;
start.value = getStart(scrollTop);
offset.value = positions.value[start.value].top;
}
同样offset偏移值使用translate3d应用到可视区域的div上面,代码如下:
<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>
</div>
</template>
<script setup>
const props = defineProps({
offset: {
type: Number,
default: 0,
},
});
const getTransform = computed(() => `translate3d(0,${props.offset}px,0)`);
</script>
这个是最终的运行效果图:
完整的父组件代码如下:
<template>
<div style="height: 100vh; width: 100vw">
<VirtualList :listData="data" :itemSize="50" />
</div>
</template>
<script setup>
import VirtualList from "./dynamic.vue";
import { faker } from "@faker-js/faker";
import { ref } from "vue";
const data = ref([]);
for (let i = 0; i < 1000; i++) {
data.value.push({
index: i,
value: faker.lorem.sentences(),
});
}
</script>
<style>
html {
height: 100%;
}
body {
height: 100%;
margin: 0;
}
#app {
height: 100%;
}
</style>
完整的虚拟列表子组件代码如下:
<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.index"
ref="itemRefs"
:data-index="item.index"
>
<span style="color: red"
>{{ item.index }}
<img width="200" :src="item.imgUrl" alt="" />
</span>
{{ item.value }}
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch, onMounted, onUpdated } from "vue";
const { listData, itemSize } = defineProps({
// 列表数据
listData: {
type: Array,
default: () => [],
},
// 预估item高度,不是真实item高度
itemSize: {
type: Number,
default: 300,
},
});
const container = ref(null);
const containerHeight = ref(0);
const start = ref(0);
const offset = ref(0);
const itemRefs = ref();
const positions = ref<
{
index: number;
height: number;
top: number;
bottom: number;
}[]
>([]);
const end = computed(() => start.value + renderCount.value);
const renderList = computed(() => listData.slice(start.value, end.value + 1));
const renderCount = computed(() => Math.ceil(containerHeight.value / itemSize));
const listHeight = computed(
() => positions.value[positions.value.length - 1].bottom
);
const getTransform = computed(() => `translate3d(0,${offset.value}px,0)`);
watch(() => listData, initPosition, {
immediate: true,
});
function handleScroll(e) {
const scrollTop = e.target.scrollTop;
start.value = getStart(scrollTop);
offset.value = positions.value[start.value].top;
}
function getStart(scrollTop) {
let left = 0;
let right = positions.value.length - 1;
while (left <= right) {
const mid = Math.floor((left + right) / 2);
if (positions.value[mid].bottom === scrollTop) {
return mid + 1;
} else if (positions.value[mid].bottom < scrollTop) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return left;
}
function initPosition() {
positions.value = [];
listData.forEach((_item, index) => {
positions.value.push({
index,
height: itemSize,
top: index * itemSize,
bottom: (index + 1) * itemSize,
});
});
}
function updatePosition() {
itemRefs.value.forEach((el) => {
const index = +el.getAttribute("data-index");
const realHeight = el.getBoundingClientRect().height;
let diffVal = positions.value[index].height - realHeight;
const curItem = positions.value[index];
if (diffVal !== 0) {
// 说明item的高度不等于预估值
curItem.height = realHeight;
curItem.bottom = curItem.bottom - diffVal;
for (let i = index + 1; i < positions.value.length - 1; i++) {
positions.value[i].top = positions.value[i].top - diffVal;
positions.value[i].bottom = positions.value[i].bottom - diffVal;
}
}
});
}
onMounted(() => {
containerHeight.value = container.value.clientHeight;
});
onUpdated(() => {
updatePosition();
});
</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>
这篇文章我们讲了不定高的虚拟列表如何实现,首先给每个item设置一个预估高度itemSize。然后根据传入的长列表数据listData初始化一个positions数组,数组中的top、bottom、height等属性表示每个item的位置。然后根据可视区域的高度加上itemSize算出可视区域内可以渲染多少renderCount个item。接着就是在onUpdated钩子函数中根据每个item的实际高度去修正positions数组中的值.
在滚动时查找第一个item的bottom大于scrollTop,这个item就是start的值。offset偏移的值为start的top属性.
值得一提的是如果不定高的列表中有图片就不能在onUpdated钩子函数中修正positions数组中的值,而是应该监听图片加载完成后再去修正positions数组。可以使用 ResizeObserver 去监听渲染的这一堆item,注意ResizeObserver的回调会触发两次,第一次为渲染item的时候,第二次为item中的图片加载完成后.
关注公众号:【前端欧阳】,给自己一个进阶vue的机会 。
另外欧阳写了一本开源电子书vue3编译原理揭秘,看完这本书可以让你对vue编译的认知有质的提升。这本书初、中级前端能看懂,完全免费,只求一个star.
最后此篇关于面试官:不会“不定高”虚拟列表,你在简历上面提他干嘛?的文章就讲到这里了,如果你想了解更多关于面试官:不会“不定高”虚拟列表,你在简历上面提他干嘛?的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
我正在寻找一种方法来创建根据价格选择我的产品的过滤器(选择下拉菜单)。 我知道这样的查询是完全可能的: SELECT * FROM products ORDER BY price ASC SELECT
函数参数中或显示尺寸时(高度,宽度)的顺序是否有约定? 最佳答案 我不知道大量的语言,但我使用过的语言(宽度,高度)。它更适合沿着 (x, y) 坐标线。 关于language-agnostic -
在我的表单中,我让用户输入房间的长度高度和宽度以获得 m2、m3 和瓦特的计算值。但是用户也应该能够直接输入 height 和 m2 来获取值。我尝试了很多语法,但 if else 不能正常工作。我知
我在 Elasticsearch 中创建了一个索引,看起来像 {"amazingdocs":{"aliases":{},"mappings":{"properties":{"Adj Close":{"
我有以下功能,我需要清除数据库中的所有图片列并移动到文件系统。当我一次性完成这一切时,内存太多并且会崩溃。我切换到递归函数并执行 20 次写入和批量操作。 我需要为大约 6 个表执行此操作。我的 Re
我正在编写一个函数来计算 PI 的值,并将其作为 double 值返回。到目前为止,一切都很好。但是一旦函数到达小数点后14位,它就不能再保存了。我假设这是因为 double 有限。我应该怎么做才能继
2020年是中国CDN行业从98年诞生到今天快速发展的第二十四年,相关数据显示,全国感知网速持续上扬,达到了3.29兆/秒,标志着在宽带中国的政策指导下,中国的网速水平正在大步赶上世界发达国家的水平
在 aerospike 集合中,我们有四个 bin userId、adId、timestamp、eventype,主键是 userId:timestamp。在 userId 上创建二级索引以获取特定用
$('#container').highcharts('Map', { title : { text : 'Highmaps basic demo'
有没有办法显示自定义宽度/高度的YouTube视频? 最佳答案 在YouTube网站上的this link中: You can resize the player by editing the obj
我使用 Highcharts ,我想在 Highcharts 状态下悬停时制作动态不同的颜色。 正如你可以看到不同的颜色,这就是我做的 var usMapChart , data = [] ; va
在所有节点上运行 tpstats 后。我看到很多节点都有大量的 ALL TIME BLOCKED NTR。我们有一个 4 节点集群,NTR ALL TIME BLOCKED 的值为: 节点 1:239
我发现 APC 上存在大量碎片 (>80%),但实际上性能似乎相当不错。我有 read another post这建议在 wordpress/w3tc 中禁用对象缓存,但我想知道减少碎片是否比首先缓存
对于我的脚本类(class),我们必须制作更高/更低的游戏。到目前为止,这是我的代码: import random seedVal = int(input("What seed should be u
我发现 APC 上存在大量碎片 (>80%),但实际上性能似乎相当不错。我有 read another post这建议在 wordpress/w3tc 中禁用对象缓存,但我想知道减少碎片是否比首先缓存
对于我的脚本类(class),我们必须制作更高/更低的游戏。到目前为止,这是我的代码: import random seedVal = int(input("What seed should be u
我已经 seen >2 字节的 unicode 代码点,如 U+10000 可以成对编写,如 \uD800\uDC00。它们似乎以半字节 d 开头,但我只注意到了这一点。 这个 split Actio
有人可以帮我理解为什么我的饼图百分比计算不正确吗?看截图: 根据我的计算,如 RHS 上所示,支出百分比应为 24.73%。传递给 Highcharts 的值如下:- 花费:204827099.36-
我阅读了有关该问题的所有答案,但我还没有找到任何解决方案。 我有一个应用程序,由我的 api 服务器提供。 Wildfly 8.1 和 Mysql 5.6。当查看时间到来时(Wildfly 服务器连接
我正在用选定的项目创建圆形导航。当用户单击任何项目时,它将移动到定义的特定点。一切都很好,除了当你继续点击项目时,当动画表现不同并且项目在 360 度圆中移动并且它被重置直到你重复场景时,我希望它
我是一名优秀的程序员,十分优秀!