背景
实习中,某个组件直接请求了大量数据时,渲染了巨多的 dom 节点,造成的界面卡顿,需要进行性能优化。
原理
通过js计算,只渲染指定可视区域内的dom节点
实现方案
- 通过监听滚动,仅实时渲染当前视口中,应该展示的数据
- 通过 css 的 translateY ,修正,当实际渲染的数据发生变化时,渲染区域与可视区域偏移的问题
- 加入缓冲数据,缓解白屏问题
相关概念
- 渲染区:真实数据 dom 节点构成的区域
- 视口区:可见区域
- 滚动容器:会出现滚动条的 dom 节点
定高虚拟列表
初始化DOM结构
定高虚拟列表
<script lang="ts">
export default {
name: 'StaticItemHeightVersion01',
}
</script>
<script setup lang="ts">
import { computed, onMounted, ref, toRefs } from 'vue'
const props = withDefaults(
defineProps<{
/**
* 每一项的高度
*/
itemHeight?: number
}>(),
{
itemHeight: 120,
}
)
const { itemHeight } = toRefs(props)
/**
* 柱子节点高度: `总数据量*每一项的高度`
*
* 用于撑开滚动容器的高度
*/
const pillarDomHeight = computed<number>(() => {
return itemHeight.value * allData.value.length
})
/**
* 所有数据
*/
const allData = ref<string[]>([])
/**
* 内容容器的y轴偏移量。当渲染区域第一个元素完全移到了可视区域之外时,需要重新计算startOffset,将第一个元素移动回可视区域
*/
const startOffset = ref<number>(0)
const styleTranslate = computed<string>(() => {
return `transform:translate(0,${startOffset.value}px)`
})
/**
* 当前视口第一个数据在allData数组的索引位置. 默认:0
*/
const start = ref<number>(0)
/**
* 当前视口最后一个数据在allData数组的索引位置
*/
const end = computed(() => {
return start.value + pageItemCount.value
})
/**
* 当前视口需要显示的数据
*/
const renderData = computed<string[]>(() => {
// 避免最后一个元素的数组下标超出实际的数组长度
const realEnd = Math.min(end.value, allData.value.length)
return allData.value.slice(start.value, realEnd)
})
/**
* 滚动容器. 支持显示滚动条的容器。确定虚拟列表的可视区高度
*/
const scrollerContainerRef = ref<HTMLDivElement>()
/**
* 滚动容器高度。采用计算属性方式动态获取滚动容器高度
*/
const scrollerContainerRefHeight = computed(() => {
return scrollerContainerRef.value ? scrollerContainerRef.value.offsetHeight : 0
})
/**
* 视口可显示的元素数量: 滚动容器高度/每一项的高度,然后对结果进行向上取整,然后再+1
*
* 为什么要进行向上取整?
* 如:页面高度100px,单个元素30px,那么此时100/30等于3,还多了10px,那这10px实际应该显示第4个元素的一小部分,所以需要进行向上取整
*
* 为什么最后还要+1?
* 如:页面高度100px,单个元素30px,根据向上取整的方式,我们已经将这个视口渲染出了4个元素,第4个元素只有10px在视口中,剩余20px在视口之外。
* 如果此时我们拖动滚动条,拖动25px,此时第一个元素尚未完全移出视口,最后一个元素完全进入了视口,且还有5px空白。按照通常的想法,这里应该显示第5个元素的一小部分才对。
* 因此,最后还需要+1
*/
const pageItemCount = computed<number>(() => {
return Math.ceil(scrollerContainerRefHeight.value / itemHeight.value) + 1
})
/**
* 模拟后端大数据获取,模拟网络延迟
*/
function loadData() {
return new Promise<string[]>(resolve => {
const tmpList: string[] = []
for (let i = 0; i < 100000; i++) {
tmpList.push(`数据${i}`)
}
setTimeout(() => {
resolve(tmpList)
}, 5000)
})
}
onMounted(() => {
// 不直接在mounted中异步转同步,而是在init方法进行异步转同步。避免界面因为数据加载慢,导致渲染阻塞
init()
})
async function init() {
allData.value = await loadData()
}
</script>
<template>
<!-- 实际开发中虚拟列表通常是位于某个dom容器下,并占满这个dom容器的整个高度,这里就是模拟这种情况 -->
<div class="outContainer">
<!-- scrollerContainer为支持滚动条的容器,定义整个虚拟列表的高度 -->
<div class="scrollerContainer" ref="scrollerContainerRef">
<div class="pillarDom" :style="{ height: `${pillarDomHeight}px` }"></div>
<div class="contentList" :style="styleTranslate">
<div class="item" v-for="oneData in renderData">{{ oneData }}</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.outContainer {
height: 350px;
width: 100%;
}
.scrollerContainer {
height: 100%;
width: 100%;
overflow: auto;
position: relative;
-webkit-overflow-scrolling: touch;
}
.pillarDom {
position: absolute;
left: 0;
top: 0;
right: 0;
z-index: -1;
}
.contentList {
position: absolute;
top: 0;
left: 0;
right: 0;
}
.item {
height: calc(v-bind(itemHeight) * 1px);
line-height: calc(v-bind(itemHeight) * 1px);
border-bottom: 8px solid green;
width: 100%;
// 这里同样很重要,盒模型必须为border-box,item元素的高度才不会因为border值而超出设置的高度
box-sizing: border-box;
background-color: orange;
&:last-child {
border-bottom: none;
}
}
</style>
监听scrollerContainer滚动使数据随着滚动发生变化
function onScroll(evt: UIEvent)
{ // 获取触发滚动事件的元素 const scrollDom = evt.target as HTMLDivElement
if (!scrollDom) return
// 获取滚动的距离
const { scrollTop } = scrollDom
// 根据滚动的距离,计算此时视口顶部需要显示的第一个元素
start.value = Math.floor(scrollTop / itemHeight.value)
}
当渲染区域的数据发生变化时,修正内容容器的偏移量,使渲染区域处于正确的视口位置
const startOffset = ref<number>(0)
const styleTranslate = computed<string>(() => { return `transform:translate(0,${startOffset.value}px)` })
} function onScroll(evt: UIEvent) {
// 获取触发滚动事件的元素 const scrollDom = evt.target as HTMLDivElement
if (!scrollDom) return
// 获取滚动的距离 const { scrollTop } = scrollDom
// 根据滚动的距离,计算此时视口顶部需要显示的第一个元素
start.value = Math.floor(scrollTop / itemHeight.value)
startOffset.value = start.value * itemHeight.value
}
这样一个基本的定高虚拟列表就完成了,但是在快速滚动的时候,出现了白屏现象,该怎么解决这个问题呢?
加入缓冲数据,缓解白屏现象
/** * 当前视口需要显示的数据 */
const renderData = computed<ContentType[]>(() => {
// 前面多缓冲一屏(避免滚动到顶部时,数组下标小于0)
let realStart = Math.max(0, start.value - pageItemCount.value)
// 后面也多换从一屏(避免最后一个元素的数组下标超出实际的数组长度)
const realEnd = Math.min(end.value + pageItemCount.value, allData.value.length)
return allData.value.slice(realStart, realEnd) })
不定高虚拟列表
初始化DOM结构
<script lang="ts">
export default {
name: 'DynItemHeightVersion01',
}
</script>
<script setup lang="ts">
import Mock from 'mockjs'
import { computed, onMounted, ref } from 'vue'
interface ContentType {
id: number
title: string
content: string
}
interface VitrualItem<T> {
/**
* 数据
*/
data: T
/**
* 当前数据处在allData数组的索引位置
*/
arrPos: number
/**
* 当前数据dom的top位置
*/
startPos: number
/**
* 当前数据dom的bottom位置
*/
endPos: number
/**
* 当前数据dom的高度(初始值为猜测高度【预估高度】)
*/
height: number
}
/**
* 猜测高度(预估高度)
*/
const maybeHeight = 100
/**
* 所有数据
*/
const allData = ref<VitrualItem<ContentType>[]>([])
/**
* 柱子节点高度: allData最后一个元素的endPos值
*
* 用于撑开滚动容器的高度
*/
const pillarDomHeight = computed<number>(() => {
return allData.value.length > 0 ? allData.value[allData.value.length - 1].endPos : 0
})
/**
* 内容列表容器dom
*/
const contentListRef = ref<HTMLDivElement>()
/**
* 滚动容器. 支持显示滚动条的容器。确定虚拟列表的可视区高度
*/
const scrollerContainerRef = ref<HTMLDivElement>()
/**
* 滚动容器高度(视口高度)。采用计算属性方式动态获取滚动容器高度
*/
const scrollerContainerRefHeight = computed(() => {
return scrollerContainerRef.value ? scrollerContainerRef.value.offsetHeight : 0
})
/**
* 当前视口第一个数据在allData数组的索引位置. 默认:0
*/
const start = ref<number>(0)
/**
* 当前视口最后一个数据在allData数组的索引位置
*/
const end = computed<number>(() => {
if (!allData.value || allData.value.length <= 0) return 0
const tmpAllData = allData.value
// 将start.value作为遍历allData的开始位置
let endPos = start.value
// contentDomTotalHeight存放从start位置开始的dom节点总高度
let contentDomTotalHeight = tmpAllData[endPos].height
// 获取视口高度
const viewPortHeight = scrollerContainerRefHeight.value
// 从start位置开始遍历allData的同时,统计数据dom节点的累计高度,直至累计高度超过了视口高度
while (contentDomTotalHeight < viewPortHeight) {
endPos++
contentDomTotalHeight += tmpAllData[endPos].height
}
// 因为数组的slice方法是包头不包尾的所以还需要再endPos上+1,才会是预期的元素数量
endPos += 1
// 因为存在在某个元素位置开区间滚动的情况,此时该元素不会完全移出视口,但又使得视口多出了位置,因此要再+1,渲染下一个元素来占满视口区域
return endPos + 1
})
/**
* 内容容器的y轴偏移量。当渲染区域第一个元素完全移到了可视区域之外时,需要重新计算startOffset,将第一个元素移动回可视区域
*/
const startOffset = ref<number>(0)
const styleTranslate = computed<string>(() => {
return `transform:translate(0,${startOffset.value}px)`
})
/**
* 当前视口需要显示的数据
*/
const renderData = computed<VitrualItem<ContentType>[]>(() => {
// 避免最后一个元素的数组下标超出实际的数组长度
const realEnd = Math.min(end.value, allData.value.length)
return allData.value.slice(start.value, realEnd)
})
function loadData() {
return new Promise<ContentType[]>(resolve => {
const data = Mock.mock({
'list|10': [
{
// 属性 id 是一个自增数,起始值为 1,每次增 1
'id|+1': 1,
title: '@ctitle(10, 20)',
content: '@cparagraph(1, 7)',
},
],
}) as { list: ContentType[] }
console.log(data.list)
resolve(data.list)
})
}
async function init() {
const tmpAllData = await loadData()
allData.value = tmpAllData.map<VitrualItem<ContentType>>((item, idx) => ({
data: item,
arrPos: idx,
startPos: maybeHeight * idx,
endPos: maybeHeight * idx + maybeHeight,
height: maybeHeight,
}))
}
onMounted(() => {
init()
})
</script>
<template>
<!-- 实际开发中虚拟列表通常是位于某个dom容器下,并占满这个dom容器的整个高度,这里就是模拟这种情况 -->
<div class="outContainer">
<!-- scrollerContainer为支持滚动条的容器,定义整个虚拟列表的高度 -->
<div class="scrollerContainer" ref="scrollerContainerRef">
<div class="pillarDom" :style="{ height: `${pillarDomHeight}px` }"></div>
<div class="contentList" :style="styleTranslate" ref="contentListRef">
<div class="item" v-for="oneData in renderData" :key="oneData.data.id" :data-index="oneData.arrPos">
<h6>{{ oneData.arrPos }} : {{ oneData.data.title }}</h6>
<p>{{ oneData.data.content }}</p>
</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.outContainer {
height: 350px;
width: 100%;
}
.scrollerContainer {
height: 100%;
width: 100%;
overflow: auto;
position: relative;
-webkit-overflow-scrolling: touch;
}
.pillarDom {
position: absolute;
left: 0;
top: 0;
right: 0;
z-index: -1;
}
.contentList {
position: absolute;
top: 0;
left: 0;
right: 0;
}
.item {
border-bottom: 8px solid green;
width: 100%;
// 这里同样很重要,盒模型必须为border-box,item元素的高度才不会因为border值而超出设置的高度
box-sizing: border-box;
background-color: orange;
padding: 5px 10px;
&:last-child {
border-bottom: none;
}
}
</style>
update 之后重新计算当前每个元素的高度和位置信息
onUpdated(() => {
const contentListDom = contentListRef.value
if (!contentListDom) return
const childrenElementArr = contentListDom.children
const dataList = allData.value for (let i = 0; i < childrenElementArr.length; i++)
{ const childEle = childrenElementArr[i] as HTMLElement
// 获取当前数据dom节点的数据再allData数组中的索引位置
const dataIndexStr = childEle.dataset['index']
if (!dataIndexStr) continue
const dataIndex = parseInt(dataIndexStr)
// 从allData数据中获取到该数据
const dataItem = dataList[dataIndex]
if (!dataItem) continue
// 获取元素的实际高度
const { height } = childEle.getBoundingClientRect()
const oldHeight = dataItem.height
/* 计算当前数据dom元素的旧高度和当前高度的差值 如: oldHeight为100px,height为50px, 那么dffVal为 50px,那么 oldHeight - dffVal 为 50px oldHeight为50px,height为100px, 那么dffVal为 -50px,那么 oldHeight - dffVal 为 100px */ const dffVal = oldHeight - height if (dffVal != 0) {
// 当前dom元素的实际高度与allData中记录的高度不一致,则更新高度以及元素位置信息
dataItem.height = oldHeight - dffVal
dataItem.endPos = dataItem.endPos - dffVal
for (let j = dataIndex + 1; j < dataList.length; j++) {
const jPosDataItem = dataList[j]
// j位置的上一个位置的元素
const jPrevPosDataItem = dataList[j - 1]
jPosDataItem.startPos = jPrevPosDataItem.endPos
jPosDataItem.endPos = jPosDataItem.startPos + jPosDataItem.height
}
}
}
allData.value = dataList })
监听滚动,并修改 start(让数据随着滚动而改变)
function onScroll(evt: UIEvent) {
const scrollerContainerDom = evt.target as HTMLDivElement
if (!scrollerContainerDom) return
const { scrollTop } = scrollerContainerDom
let idx = 0
const dataList = allData.value
let dataItem = dataList[idx]
while (dataItem.endPos <= scrollTop) {
idx++ dataItem = dataList[idx] }
start.value = idx
// console.log(start.value) }
监听滚动,并修正内容容器的偏移量
function onScroll(evt: UIEvent) {
const scrollerContainerDom = evt.target as HTMLDivElement
if (!scrollerContainerDom) return
const { scrollTop } = scrollerContainerDom
let idx = 0
const dataList = allData.value
let dataItem = dataList[idx]
while (dataItem.endPos <= scrollTop) {
idx++
dataItem = dataList[idx] }
start.value = idx
startOffset.value = allData.value[start.value].startPos }
</script>
这样,一个基本的不定高的虚拟列表已经实现,但是在滚动中出现了很明显的卡顿,使用Chrome浏览器的Performance选项卡进行分析,发现脚本分析占用了大部分的时间,其中update之后重新计算这个函数是问题所在,猜测是因为这个频繁的访问了响应式变量,于是便做了以下的优化
修正大数据量卡顿问题
<script lang="ts">
export default {
name: 'DynItemHeightVersion06',
}
</script>
<script setup lang="ts">
import Mock from 'mockjs'
import { computed, markRaw, nextTick, onMounted, onUpdated, ref } from 'vue'
interface ContentType {
id: number
title: string
content: string
arrPos: number
}
interface ContentPosition {
/**
* 当前数据处在allData数组的索引位置
*/
arrPos: number
/**
* 当前数据dom的top位置
*/
startPos: number
/**
* 当前数据dom的bottom位置
*/
endPos: number
/**
* 当前数据dom的高度(初始值为猜测高度【预估高度】)
*/
height: number
}
/**
* 猜测高度(预估高度)
*/
const maybeHeight = 100
/**
* 所有数据
*/
const allData = ref<ContentType[]>([])
let positionDataArr: ContentPosition[] = []
/**
* 柱子节点高度: allData最后一个元素的endPos值
*
* 用于撑开滚动容器的高度
*/
const pillarDomHeight = ref<number>(0)
/**
* 内容列表容器dom
*/
const contentListRef = ref<HTMLDivElement>()
/**
* 滚动容器. 支持显示滚动条的容器。确定虚拟列表的可视区高度
*/
const scrollerContainerRef = ref<HTMLDivElement>()
/**
* 滚动容器高度(视口高度)。采用计算属性方式动态获取滚动容器高度
*/
const scrollerContainerRefHeight = computed(() => {
return scrollerContainerRef.value ? scrollerContainerRef.value.offsetHeight : 0
})
/**
* 当前视口第一个数据在allData数组的索引位置. 默认:0
*/
const start = ref<number>(0)
/**
* 当前视口最后一个数据在positionDataArr数组的索引位置
*/
const end = computed<number>(() => {
if (!allData.value || allData.value.length <= 0) return 0
// 将start.value作为遍历positionDataArr的开始位置
let endPos = start.value
// contentDomTotalHeight存放从start位置开始的dom节点总高度
let contentDomTotalHeight = positionDataArr[endPos].height
// 获取视口高度
const viewPortHeight = scrollerContainerRefHeight.value
// 从start位置开始遍历positionDataArr的同时,统计数据dom节点的累计高度,直至累计高度超过了视口高度
while (contentDomTotalHeight < viewPortHeight) {
endPos++
contentDomTotalHeight += positionDataArr[endPos].height
}
// 因为数组的slice方法是包头不包尾的所以还需要再endPos上+1,才会是预期的元素数量
endPos += 1
// 因为存在在某个元素位置开区间滚动的情况,此时该元素不会完全移出视口,但又使得视口多出了位置,因此要再+1,渲染下一个元素来占满视口区域
return endPos + 1
})
/**
* 内容容器的y轴偏移量。当渲染区域第一个元素完全移到了可视区域之外时,需要重新计算contentListOffset,将第一个元素移动回可视区域
*/
const contentListOffset = ref<number>(0)
const styleTranslate = computed<string>(() => {
return `transform:translate(0,${contentListOffset.value}px)`
})
/**
* 当前视口需要显示的数据
*/
const renderData = computed<ContentType[]>(() => {
// 避免最后一个元素的数组下标超出实际的数组长度
const realEnd = Math.min(end.value, allData.value.length)
return allData.value.slice(start.value, realEnd)
})
function loadData() {
return new Promise<ContentType[]>(resolve => {
const data = Mock.mock({
'list|10000': [
{
// 属性 id 是一个自增数,起始值为 1,每次增 1
'id|+1': 1,
title: '@ctitle(10, 20)',
content: '@cparagraph(1, 7)',
},
],
}) as { list: ContentType[] }
resolve(data.list)
})
}
async function init() {
const tmpArr = await loadData()
allData.value = tmpArr.map<ContentType>((item, idx) => markRaw({ ...item, arrPos: idx }))
positionDataArr = allData.value.map<ContentPosition>((_, idx) => ({
arrPos: idx,
startPos: maybeHeight * idx,
endPos: maybeHeight * idx + maybeHeight,
height: maybeHeight,
}))
}
onMounted(() => {
init()
})
function updateHeightAndPos() {
const contentListDom = contentListRef.value
if (!contentListDom) return
const childrenElementArr = contentListDom.children
for (let i = 0; i < childrenElementArr.length; i++) {
const childEle = childrenElementArr[i] as HTMLElement
// 获取当前数据dom节点的数据在positionDataArr数组中的索引位置
const dataIndexStr = childEle.dataset['index']
if (!dataIndexStr) continue
const dataIndex = parseInt(dataIndexStr)
// 从positionDataArr获取到当前dom元素的位置信息和高度信息
const dataItem = positionDataArr[dataIndex]
if (!dataItem) continue
// 获取元素的实际高度
const { height } = childEle.getBoundingClientRect()
// const { offsetHeight: height } = childEle
const oldHeight = dataItem.height
/*
计算当前数据dom元素的旧高度和当前高度的差值
如:
oldHeight为100px,height为50px, 那么dffVal为 50px,那么 oldHeight - dffVal 为 50px
oldHeight为50px,height为100px, 那么dffVal为 -50px,那么 oldHeight - dffVal 为 100px
*/
const dffVal = oldHeight - height
if (dffVal != 0) {
// 当前dom元素的实际高度与allData中记录的高度不一致,则更新高度以及元素位置信息
dataItem.height = oldHeight - dffVal
dataItem.endPos = dataItem.endPos - dffVal
for (let j = dataIndex + 1; j < positionDataArr.length; j++) {
const jPosDataItem = positionDataArr[j]
// j位置的上一个位置的元素
const jPrevPosDataItem = positionDataArr[j - 1]
jPosDataItem.startPos = jPrevPosDataItem.endPos
jPosDataItem.endPos = jPosDataItem.startPos + jPosDataItem.height
}
}
}
pillarDomHeight.value = positionDataArr.length > 0 ? positionDataArr[positionDataArr.length - 1].endPos : 0
}
onUpdated(() => {
nextTick(() => {
updateHeightAndPos()
})
})
function onScroll(evt: UIEvent) {
const scrollerContainerDom = evt.target as HTMLDivElement
if (!scrollerContainerDom) return
const { scrollTop } = scrollerContainerDom
let idx = 0
let dataItem = positionDataArr[idx]
while (dataItem.endPos <= scrollTop) {
idx++
dataItem = positionDataArr[idx]
}
start.value = idx
contentListOffset.value = positionDataArr[start.value].startPos
}
</script>
<template>
<!--
为什么 pillarDomHeight 值在滚动到最后再往回滚的时候还是变化的,
按理说此时所有元素都真实渲染过一次的获取过一次真实高度,
那 pillarDomHeight 取的是最后一个元素的endPos,那pillarDomHeight的值应该不变了才对呀?
因为css样式中:有个设置 &:last-child {border-bottom: none;}, 那么就会导致某个元素处于最后一个元素时没有border-bottom,
而处于非最后一个元素时有border-bottom,所以数据项dom的高度在这个层面也是动态变化的
那人可能还会想,还是不对呀,你不是给item元素设置box-sizing: border-box;吗?这个盒模型当有border的时候不是也不会撑开元素高度吗?
是的,但那得元素本身有设置明确的高度的情况,如果元素本身没有明确的设置高度,元素的实际高度依然会被border撑开
如何验证你上面的说法?
将 &:last-child {border-bottom: none;} 注释掉,再滚动到底部,再来回滚动,你就会发现 pillarDomHeight 已经是一个固定值了
-->
{{ pillarDomHeight }}
<!-- 实际开发中虚拟列表通常是位于某个dom容器下,并占满这个dom容器的整个高度,这里就是模拟这种情况 -->
<div class="outContainer">
<!-- scrollerContainer为支持滚动条的容器,定义整个虚拟列表的高度 -->
<div class="scrollerContainer" ref="scrollerContainerRef" @scroll="onScroll">
<div class="pillarDom" :style="{ height: `${pillarDomHeight}px` }"></div>
<div class="contentList" :style="styleTranslate" ref="contentListRef">
<div class="item" v-for="oneData in renderData" :key="oneData.id" :data-index="oneData.arrPos">
<h6>{{ oneData.arrPos }} : {{ oneData.title }}</h6>
<p>{{ oneData.content }}</p>
</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.outContainer {
height: 350px;
width: 100%;
}
.scrollerContainer {
height: 100%;
width: 100%;
overflow: auto;
position: relative;
-webkit-overflow-scrolling: touch;
}
.pillarDom {
position: absolute;
left: 0;
top: 0;
right: 0;
z-index: -1;
}
.contentList {
position: absolute;
top: 0;
left: 0;
right: 0;
}
.item {
border-bottom: 8px solid green;
width: 100%;
// 这里同样很重要,盒模型必须为border-box,item元素的高度才不会因为border值而超出设置的高度
box-sizing: border-box;
background-color: orange;
padding: 5px 10px;
&:last-child {
border-bottom: none;
}
}
</style>
使用二分查找优化获取 start 值的方式
/**
* 通过二分查找来获取start值
*
* @param {ContentPosition[]} _positionDataArr [_positionDataArr description]
* @param {number} scrollTop [scrollTop description]
*
* @return {[]} [return description]
*/
function findStartByBinarySearch(_positionDataArr: ContentPosition[], scrollTop: number) {
let startIdx = 0
let endIdx = _positionDataArr.length - 1
let resultIdx: number | undefined
while (startIdx <= endIdx) {
// Math.trunc 去除小数部分,只取整数部分. 取startIdx 到 endIdx的中间索引号
const middleIdx = Math.trunc((startIdx + endIdx) / 2)
// 获取中间索引号对应元素的位置信息
const middleEle = _positionDataArr[middleIdx]
// 获取中间索引号对应元素的底部位置
const middleEleEndPos = middleEle.endPos
if (middleEleEndPos === scrollTop) {
// 当前滚动高度等于中间索引号对应元素的底部位置,则start为中间索引号的下一个位置
return middleIdx + 1
} else if (middleEleEndPos < scrollTop) {
// 当前滚动高度大于中间索引号对应元素的底部位置,则调整查找区间为右区间
startIdx = middleIdx + 1
} else if (middleEleEndPos > scrollTop) {
// 当前滚动高度大于中间索引号对应元素的底部位置,则调整查找区间为左区间
if (resultIdx === undefined || resultIdx > middleIdx) {
// 存储元素 middleEleEndPos>scrollTop 元素的最小数组索引号
resultIdx = middleIdx
}
// 调整查找区间为左区间
endIdx = middleIdx - 1
}
}
return resultIdx
}