一、元素固定高度的虚拟列表
<template>
<FixedSizeList
className="list"
:height="200"
:width="200"
:itemSize="50"
:itemCount="1000"
/>
</template>
<script setup>
import FixedSizeList from './components/FixedSizeList.vue'
</script>
<script setup>
import { ref } from 'vue'
import Row from './Row.vue'
const props = defineProps(['height', 'width', 'itemSize', 'itemCount'])
const scrollOffset = ref(0)
const containerStyle = {
position: 'relative',
width: props.width + 'px',
height: props.height + 'px',
overflow: 'auto',
}
const contentStyle = {
height: props.itemSize * props.itemCount + 'px',
width: '100%',
}
const getCurrentChildren = () => {
const startIndex = Math.floor(scrollOffset.value / props.itemSize)
const finialStartIndex = Math.max(0, startIndex - 2)
const numVisible = Math.ceil(props.height / props.itemSize)
const endIndex = Math.min(props.itemCount - 1, startIndex + numVisible + 2)
const items = []
for (let i = finialStartIndex; i < endIndex; i++) {
const itemStyle = {
position: 'absolute',
height: props.itemSize + 'px',
width: '100%',
top: props.itemSize * i + 'px',
idx: i,
}
items.push(itemStyle)
}
return items
}
const currentChildren = ref([])
currentChildren.value = getCurrentChildren()
const scrollHandle = (event) => {
const { scrollTop } = event.currentTarget
scrollOffset.value = scrollTop
currentChildren.value = getCurrentChildren()
}
</script>
<template>
<div :style="containerStyle" :onScroll="scrollHandle">
<div :style="contentStyle">
<Row
v-for="item in currentChildren"
:key="item.idx"
:index="item.idx"
:style="item"
/>
</div>
</div>
</template>
<script setup>
const props = defineProps(['index', 'style'])
</script>
<template>
<div :style="style">
{{ `${index} 🥇 ` }}
</div>
</template>
二、元素不定高度的虚拟列表
<template>
<VariableSizeList
className="list"
:height="200"
:width="200"
:itemSize="getItemSize"
:itemCount="1000"
/>
</template>
<script setup>
import FixedSizeList from './components/FixedSizeList.vue'
const rowSizes = new Array(1000)
.fill(true)
.map(() => 25 + Math.round(Math.random() * 55))
const getItemSize = (index) => rowSizes[index]
</script>
<script setup>
import { ref } from 'vue'
import Row from './Row.vue'
const measuredData = {
measuredDataMap: {},
lastMeasuredItemIndex: -1,
}
const estimatedHeight = (defaultEstimatedItemSize = 50, itemCount) => {
let measuredHeight = 0
const { measuredDataMap, lastMeasuredItemIndex } = measuredData
if (lastMeasuredItemIndex >= 0) {
const lastMeasuredItem = measuredDataMap[lastMeasuredItemIndex]
measuredHeight = lastMeasuredItem.offset + lastMeasuredItem.size
}
const unMeasuredItemsCount =
itemCount - measuredData.lastMeasuredItemIndex - 1
const totalEstimatedHeight =
measuredHeight + unMeasuredItemsCount * defaultEstimatedItemSize
return totalEstimatedHeight
}
const getItemMetaData = (props, index) => {
const { itemSize } = props
const { measuredDataMap, lastMeasuredItemIndex } = measuredData
if (index > lastMeasuredItemIndex) {
let offset = 0
if (lastMeasuredItemIndex >= 0) {
const lastMeasuredItem = measuredDataMap[lastMeasuredItemIndex]
offset += lastMeasuredItem.offset + lastMeasuredItem.size
}
for (let i = lastMeasuredItemIndex + 1; i <= index; i++) {
const currentItemSize = itemSize(i)
measuredDataMap[i] = { size: currentItemSize, offset }
offset += currentItemSize
}
measuredData.lastMeasuredItemIndex = index
}
return measuredDataMap[index]
}
const getStartIndex = (props, scrollOffset) => {
let index = 0
while (true) {
const currentOffset = getItemMetaData(props, index).offset
if (currentOffset >= scrollOffset) return index
index++
}
}
const getEndIndex = (props, startIndex) => {
const { height } = props
const startItem = getItemMetaData(props, startIndex)
const maxOffset = startItem.offset + height
let offset = startItem.offset + startItem.size
let endIndex = startIndex
while (offset <= maxOffset) {
endIndex++
const currentItem = getItemMetaData(props, endIndex)
offset += currentItem.size
}
return endIndex
}
const getRangeToRender = (props, scrollOffset) => {
const { itemCount } = props
const startIndex = getStartIndex(props, scrollOffset)
const endIndex = getEndIndex(props, startIndex)
return [
Math.max(0, startIndex - 2),
Math.min(itemCount - 1, endIndex + 2),
startIndex,
endIndex,
]
}
const props = defineProps([
'height',
'width',
'itemCount',
'itemSize',
'itemEstimatedSize',
])
const scrollOffset = ref(0)
const containerStyle = {
position: 'relative',
width: props.width + 'px',
height: props.height + 'px',
overflow: 'auto',
willChange: 'transform',
}
const contentStyle = {
height: estimatedHeight(props.itemEstimatedSize, props.itemCount) + 'px',
width: '100%',
}
const getCurrentChildren = () => {
const [startIndex, endIndex, originStartIndex, originEndIndex] =
getRangeToRender(props, scrollOffset.value)
const items = []
for (let i = startIndex; i < endIndex; i++) {
const item = getItemMetaData(props, i)
const itemStyle = {
position: 'absolute',
height: item.size + 'px',
width: '100%',
top: item.offset + 'px',
idx: i,
}
items.push(itemStyle)
}
return items
}
const currentChildren = ref([])
currentChildren.value = getCurrentChildren()
const scrollHandle = (event) => {
const { scrollTop } = event.currentTarget
scrollOffset.value = scrollTop
currentChildren.value = getCurrentChildren()
}
</script>
<template>
<div id="container" :style="containerStyle" :onScroll="scrollHandle">
<div id="content" :style="contentStyle">
<Row
v-for="item in currentChildren"
:key="item.idx"
:index="item.idx"
:style="item"
/>
</div>
</div>
</template>
<script setup>
const props = defineProps(['index', 'style'])
</script>
<template>
<div :style="style">
{{ `${index} 🥇 ` }}
</div>
</template>
三、元素动态高度的虚拟列表
<template>
<FixedSizeListPlus
class="list"
:height="200"
:width="200"
:itemCount="items.length"
:raw="getOneOfData"
/>
</template>
<script setup>
import FixedSizeListPlus from './components/FixedSizeListPlus.vue'
function getData(fixedHeight = 50) {
const textStr = `谁说简历没亮点?来了,虚拟滚动的3种实现方式~ React实现方式`
const items = []
const itemCount = 1000
for (let i = 0; i < itemCount; i++) {
const style = {
width: '100%',
height: fixedHeight,
}
items.push({
style: style,
i,
value: textStr.slice(parseInt(Math.random() * 30)),
})
}
return items
}
const items = getData()
const getOneOfData = ({ index }) => {
return items[index]
}
</script>
<script setup>
import { ref, onMounted } from 'vue'
import Row from './Row.vue'
const measuredData = {
measuredDataMap: {},
lastMeasuredItemIndex: -1,
}
const estimatedHeight = (defaultEstimatedItemSize = 50, itemCount) => {
let measuredHeight = 0
const { measuredDataMap, lastMeasuredItemIndex } = measuredData
if (lastMeasuredItemIndex >= 0) {
const lastMeasuredItem = measuredDataMap[lastMeasuredItemIndex]
measuredHeight = lastMeasuredItem.offset + lastMeasuredItem.size
}
const unMeasuredItemsCount =
itemCount - measuredData.lastMeasuredItemIndex - 1
const totalEstimatedHeight =
measuredHeight + unMeasuredItemsCount * defaultEstimatedItemSize
return totalEstimatedHeight
}
const getItemMetaData = (props, index) => {
const { itemSize } = props
const { measuredDataMap, lastMeasuredItemIndex } = measuredData
if (index > lastMeasuredItemIndex) {
let offset = 0
if (lastMeasuredItemIndex >= 0) {
const lastMeasuredItem = measuredDataMap[lastMeasuredItemIndex]
offset += lastMeasuredItem.offset + lastMeasuredItem.size
}
for (let i = lastMeasuredItemIndex + 1; i <= index; i++) {
const currentItemSize = itemSize ? itemSize(i) : 50
measuredDataMap[i] = { size: currentItemSize, offset }
offset += currentItemSize
}
measuredData.lastMeasuredItemIndex = index
}
return measuredDataMap[index]
}
const getStartIndex = (props, scrollOffset) => {
let index = 0
while (true) {
const currentOffset = getItemMetaData(props, index).offset
if (currentOffset >= scrollOffset) return index
index++
}
}
const getEndIndex = (props, startIndex) => {
const { height } = props
const startItem = getItemMetaData(props, startIndex)
const maxOffset = startItem.offset + height
let offset = startItem.offset + startItem.size
let endIndex = startIndex
while (offset <= maxOffset) {
endIndex++
const currentItem = getItemMetaData(props, endIndex)
offset += currentItem.size
}
return endIndex
}
const getRangeToRender = (props, scrollOffset) => {
const { itemCount } = props
const startIndex = getStartIndex(props, scrollOffset)
const endIndex = getEndIndex(props, startIndex)
return [
Math.max(0, startIndex - 2),
Math.min(itemCount - 1, endIndex + 2),
startIndex,
endIndex,
]
}
const props = defineProps([
'height',
'width',
'itemCount',
'itemSize',
'itemEstimatedSize',
'raw',
])
const scrollOffset = ref(0)
const containerStyle = {
position: 'relative',
width: props.width + 'px',
height: props.height + 'px',
overflow: 'auto',
willChange: 'transform',
}
const contentStyle = {
height: estimatedHeight(props.itemEstimatedSize, props.itemCount) + 'px',
width: '100%',
}
const getCurrentChildren = () => {
const [startIndex, endIndex, originStartIndex, originEndIndex] =
getRangeToRender(props, scrollOffset.value)
const items = []
for (let i = startIndex; i < endIndex; i++) {
const item = getItemMetaData(props, i)
const itemStyle = {
position: 'absolute',
width: '100%',
top: item.offset + 'px',
idx: i,
}
items.push(itemStyle)
}
return items
}
const currentChildren = ref([])
currentChildren.value = getCurrentChildren()
onMounted(() => {
currentChildren.value = getCurrentChildren()
})
const sizeChangeHandle = (index, domNode) => {
const height = domNode.offsetHeight
const { measuredDataMap, lastMeasuredItemIndex } = measuredData
const itemMetaData = measuredDataMap[index]
itemMetaData.size = height
let offset = 0
for (let i = 0; i <= lastMeasuredItemIndex; i++) {
const itemMetaData = measuredDataMap[i]
itemMetaData.offset = offset
offset += itemMetaData.size
}
}
const scrollHandle = (event) => {
const { scrollTop } = event.currentTarget
scrollOffset.value = scrollTop
currentChildren.value = getCurrentChildren()
}
</script>
<template>
<div id="container" :style="containerStyle" @scroll="scrollHandle">
<div id="content" :style="contentStyle">
<Row
v-for="item in currentChildren"
:key="item.idx"
:index="item.idx"
:style="item"
:onSizeChange="sizeChangeHandle"
:raw="raw"
/>
</div>
</div>
</template>
<style scoped>
#container #content > div:nth-child(odd) {
background: rgba(90, 118, 211, 0.5);
}
</style>
<script setup>
const props = defineProps(['index', 'style', 'raw', 'onSizeChange'])
const domRef = ref(null)
onMounted(() => {
const { index, onSizeChange } = props
onSizeChange?.(index, domRef.value)
})
</script>
<template>
<div ref="domRef" :style="style">
{{ `${index} 🥇 ${raw?.({ index }).value || ''}` }}
</div>
</template>