feat: 定高和不定高虚拟列表参考资料

master
LCJ-MinYa 5 months ago
parent a5c3252087
commit 6d5576c0c9

Binary file not shown.

After

Width:  |  Height:  |  Size: 331 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 350 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 357 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 339 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 291 KiB

@ -0,0 +1,18 @@
<template>
<div
class="markdown-body"
v-html="htmlStr"
/>
</template>
<script setup>
import { ref } from 'vue';
import { marked } from 'marked';
import { getMarkdownContent } from '@/utils/tools';
const htmlStr = ref('');
getMarkdownContent('./md/vListDoc1.md').then((res) => {
htmlStr.value = marked(res);
});
</script>

@ -0,0 +1,266 @@
# 给我2分钟保证教会你在Vue3中实现一个定高的虚拟列表
# 前言
虚拟列表对于大部分一线开发同学来说是一点都不陌生的东西了有的同学是直接使用第三方组件。但是面试时如果你简历上面写了虚拟列表却给面试官说是通过三方组件实现的此时空气可能都凝固了。所以这篇文章欧阳将会教你2分钟内实现一个定高的虚拟列表至于不定高的虚拟列表下一篇文章来写。
# 什么是虚拟列表
有的特殊场景我们不能分页,只能渲染一个长列表。这个长列表中可能有几万条数据,如果全部渲染到页面上用户的设备差点可能就会直接卡死了,这时我们就需要虚拟列表来解决问题。
一个常见的虚拟列表是下面这样的,如下图:
![v1](/src/views/demo/vListDoc1/img/1.png)
其中实线框的item表示在视口区域内真实渲染DOM虚线框的item表示并没有渲染的DOM。
在定高的虚拟列表中,我们可以根据`可视区域的高度`和`每个item的高度`计算得出在可视区域内可以渲染多少个item。不在可视区域里面的item那么就不需要渲染了不管有几万个还是几十万个item这样就能解决长列表性能很差的问题啦。
# 实现滚动条
按照上面的图很容易想到我们的dom结构应该是下面这样的
```javascript
<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`,这样可视区域就被撑开了,并且滚动条也有了。代码如下:
```javascript
<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 = 0`和`renderCount = 2`,我们设置的是`end = 2`实际是渲染了3个item。目的是为了预渲染下一个后面会讲。
# 监听滚动事件
有了滚动条后就可以开始滚动了,我们监听`container`容器的scroll事件。
可视区域中的内容应该随着滚动条的滚动而变化也就是说在scroll事件中我们需要重新计算`start`的值。
```javascript
function handleScroll(e) {
const scrollTop = e.target.scrollTop;
start.value = Math.floor(scrollTop / itemSize);
offset.value = scrollTop - (scrollTop % itemSize);
}
```
如果当前`itemSize`的值为100。
如果此时滚动的距离在0-100之间比如下面这样
![v1](/src/views/demo/vListDoc1/img/2.png)
上面这张图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);`如下图:
![v1](/src/views/demo/vListDoc1/img/3.png)
此时的`start`从item2开始渲染但是由于前面我们复用了浏览器的滚动所以实际渲染的DOM第一个已经在可视区外面了。此时可视区看见的第一个是item3很明显是不对的应该看见的是第一个是item2。
此时应该怎么办呢?
很简单,使用`translate`将列表向下偏移一个item的高度就行也就是100px。列表偏移后就是下面这样的了
![v1](/src/views/demo/vListDoc1/img/4.png)
如果当前`scrollTop`的值为200那么偏移值就是200px。所以我们得出
```javascript
offset.value = scrollTop - (scrollTop % itemSize);
```
为什么这里要减去`scrollTop % itemSize`呢?
因为在滚动时如果是在item的高度范围内滚动我们是复用浏览器的滚动此时无需进行偏移所以计算偏移值时需要减去`scrollTop % itemSize`。
实际上从一个item滚动到另外一个item时比如从`item0`滚动到`item1`。此时会做两件事情:将`start`的值从`0`更新为`1`和根据`scrollTop`计算得到列表的偏移值`100`从而让新的start对应的`item1`重新回到可视范围内。
这个是运行效果图:
![v1](/src/views/demo/vListDoc1/img/5.gif)
下面是完整的代码:
```javascript
<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>
```
这个是父组件的代码:
```javascript
<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重新回到可视范围内。

Binary file not shown.

After

Width:  |  Height:  |  Size: 337 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 MiB

@ -0,0 +1,18 @@
<template>
<div
class="markdown-body"
v-html="htmlStr"
/>
</template>
<script setup>
import { ref } from 'vue';
import { marked } from 'marked';
import { getMarkdownContent } from '@/utils/tools';
const htmlStr = ref('');
getMarkdownContent('./md/vListDocs2.md').then((res) => {
htmlStr.value = marked(res);
});
</script>

@ -0,0 +1,463 @@
# 面试官:不会“不定高”虚拟列表,你在简历上面提他干嘛?
# 前言
很多同学将虚拟列表当做亮点写在简历上面,但是却不知道如何手写,那么这个就不是加分项而是减分项了。在上一篇文章欧阳教会你 [如何实现一个定高虚拟列表](https://mp.weixin.qq.com/s/unNbvl6L6vLHXcnyheI1UQ) ,但是实际项目中更多的是`不定高虚拟列表`这篇文章欧阳来教你不定高如何实现。PS建议先看看欧阳的上一篇 [如何实现一个定高虚拟列表](https://mp.weixin.qq.com/s/unNbvl6L6vLHXcnyheI1UQ) 后再来看这篇效果更佳。
# 什么是不定高虚拟列表
不定高的意思很简单就是不知道每一项item的具体高度如下图
![v1](/src/views/demo/vListDoc2/img/1.png)
现在我们有个问题,**在不定高的情况下我们就不能根据当前滚动条的`scrollTop`去计算可视区域里面实际渲染的第一个item的index位置也就是`start`的值。**
没有`start`那么就无法实现在滚动的时候只渲染可视区域的那几个item了。
# 预估高度
既然我们不知道每个item的高度那么就采用`预估高度`的方式去实现。比如这样:
```javascript
const { listData, itemSize } = defineProps({
// 列表数据
listData: {
type: Array,
default: () => [],
},
// 预估item高度不是真实item高度
itemSize: {
type: Number,
default: 300,
},
});
```
还是和上一篇一样的套路,计算出当前可视区域的高度`containerHeight`,然后结合预估的`itemSize`就可以得到当前可视区域里面渲染的item数量。代码如下
```javascript
const renderCount = computed(() => Math.ceil(containerHeight.value / itemSize));
```
注意:由于我们是预估的高度,所以这个`renderCount`的数量是不准的。
如果预估的高度比实际高太多那么实际渲染的item数量就会不够导致页面下方出现白屏的情况。
如果预估的高度太小那么这里的item数量就会渲染的太多了性能又没之前那么好。
所以预估item高度需要根据实际业务去给一个适当的值理论上是宁可预估小点也不预估的大了大了会出现白屏
start初始值为0并且算出了`renderCount`,此时我们也就知道了可视区域渲染的最后一个`end`的值。如下:
```javascript
const end = computed(() => start.value + renderCount.value);
```
和上一篇一样计算end时在下方多渲染了一个item第一个item有一部分滚出可视区域的情况时如果不多渲染可能就会出现白屏的情况。
有了`start`和`end`,那么就知道了可视区域渲染的`renderList`,代码如下:
```javascript
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`定义如下:
```javascript
const positions = ref<
{
index: number;
height: number;
top: number;
bottom: number;
}[]
>([]);
```
`positions`的初始化值为空数组,那么什么时候给这个数组赋值呢?
答案很简单虚拟列表渲染的是props传入进来的`listData`。所以我们watch监听`listData`,加上`immediate: true`。这样就可以实现初始化时给`positions`赋值,代码如下:
```javascript
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`中的值后他就是越来越准的了。
所以列表的真实高度为:
```javascript
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`了。代码如下:
```javascript
<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偏移量的值。如果你看不懂这句话建议先看我上一篇[如何实现一个定高虚拟列表](https://mp.weixin.qq.com/s/unNbvl6L6vLHXcnyheI1UQ) 文章。**
此时应该如何计算最新的`start`值呢?
很简单!在`positions`中存了两个字段分别是`top`和`bottom`分别表示当前item的`开始位置`和`结束位置`。如果当前滚动条的`scrollTop`刚好在`top`和`bottom`之间,也就是`scrollTop >= top && scrollTop < bottom`item
并且由于在`positions`数组中`bottom`的值是递增的那么问题不就变成了查找第一个item的`scrollTop < bottom`
```javascript
function getStart(scrollTop) {
return positions.value.findIndex((item) => scrollTop < item.bottom);
}
```
每次scroll滚动都会触发一次这个查找那么我们可以优化上面的算法吗
`positions`数组中的`bottom`字段是递增的,这很符合`二分查找`的规律。不了解二分查找的同学可以看看leetcode上面的这道题 https://leetcode.cn/problems/search-insert-position/description/。
所以上面的代码可以优化成这样:
```javascript
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`的值为:
```javascript
offset.value = positions.value[start.value].top;
```
可能有的小伙伴会迷惑,在`start`的item中的滚动值为什么不算到`offset`偏移中去呢?
因为在`start`的item范围内滚动时都是直接使用的浏览器滚动已经有了scrollTop所以无需加到`offset`偏移中去。
所以我们得出当scroll事件触发时代码如下
```javascript
function handleScroll(e) {
const scrollTop = e.target.scrollTop;
start.value = getStart(scrollTop);
offset.value = positions.value[start.value].top;
}
```
同样`offset`偏移值使用`translate3d`应用到可视区域的div上面代码如下
```javascript
<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>
```
这个是最终的运行效果图:
![v1](/src/views/demo/vListDoc2/img/2.gif)
完整的父组件代码如下:
```javascript
<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>
```
完整的虚拟列表子组件代码如下:
```javascript
<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](https://developer.mozilla.org/zh-CN/docs/Web/API/ResizeObserver) 去监听渲染的这一堆item注意`ResizeObserver`的回调会触发两次第一次为渲染item的时候第二次为item中的图片加载完成后。
Loading…
Cancel
Save