这里给大家分享我在网上总结出来的一些知识 ,希望对大家有所帮助
<div class="virtual-list">
<!-- 这里是用于撑开高度 ,出现滚动条用 -->
<div class="list-view-phantom" ref="clientHeightRef" :style="{ height: list.length*itemHeight + px }"></div>
<ul v-if="list.length > 0" class="option-warp" ref="contentRef">
<li
:style="{ height: itemHeight + px }"
class="option"
v-for="(item, index) in virtualRenderData"
:key="index"
>
{{item}}
</li>
</ul>
</div>
每一条数据的高度是this.itemHeight=10,渲染容器的高度是this.containerHeight=300,那么一屏需要渲染的数据是count=Math.ceil(this.containerHeight / this.itemHeight) 。
假设我们我们的需要的数据list如下:
const list = [
{id:1,name:1},
{id:2,name:3},
....
]
那么撑开滚动条的容器的高度是this.list.length*this.itemHeight 。
我们给渲染容器加一个监听滚动的事件,主要是获取当前滚动的scrollTop ,用来更新渲染可视区域的数据。如下 ,我们封装一个更新渲染可视区域的数据函数:
const update = function(scrollTop = 0){
this.$nextTick(() => {
// 获取当前可展示数量
const count = Math.ceil(this.containerHeight / this.itemHeight)
const start = Math.floor(scrollTop / this.itemHeight)
// 取得可见区域的结束数据索引
const end = start + count
// 计算出可见区域对应的数据 ,让 Vue.js 更新
this.virtualRenderData = this.list.slice(start, end)
})
}
当滚动条滚动的时候 ,我们需要从list中截取当前渲染容器刚刚好可以渲染的数据 ,达到像真的滚动的了一样 。上面的滚动函数虽然已经更新了渲染可视区域的数据 ,但是当我们滚动的时候会发现内容块被滚动到了上面 ,再次滚动的时候直接就不见了 。这是由于滚动条是由撑开滚动条的容器撑开的 ,渲染的内容高度只有容器的高度 ,所以它只会在顶部出现,滚动的时候自然就不会动 ,效果如下:
所以当我们滚动滚动的时候 ,还需要将渲染内容往对应的方向偏移 。比如偏移的y方向距离就是scrollTop的距离,
this.$refs.contentRef.style.webkitTransform = `translate3d(0, ${scrollTop * this.itemHeight}px, 0)`
这样就能达到滚动的时候 ,渲染内容始终保持在渲染容器的顶部 ,好像真的随着滚动条滚动而滚动 。
但是实际上这样的效果是不好的,因为我们更新的颗粒度是按每一条数据来分的 ,而不是按scrollTop来进行的 ,所以渲染内容的偏移量也需要按照每一条数据的颗粒度来进行更新 ,代码如下:
const update = function(scrollTop = 0){
this.$nextTick(() => {
// 获取当前可展示数量
const count = Math.ceil(this.containerHeight / this.itemHeight)
const start = Math.floor(scrollTop / this.itemHeight)
// 取得可见区域的结束数据索引
const end = start + count
// 计算出可见区域对应的数据 ,让 Vue.js 更新
this.virtualRenderData = this.list.slice(start, end)
+ this.$refs.contentRef.style.webkitTransform = `translate3d(0, ${start * this.itemHeight}px, 0)`
})
}
上面的代码基本可以满足基本的使用 ,但是当我们滚动比较快的时候 ,渲染区域底部会出现瞬间留白 ,是因为dom没有及时的渲染 ,原因是我们只渲染刚刚好一屏的数据 。
为了减少留白的出现 ,我们应该预渲染几条数据bufferCount,增加渲染缓存区间:
const update = function(scrollTop = 0){
this.$nextTick(() => {
// 获取当前可展示数量
const count = Math.ceil(this.containerHeight / this.itemHeight)
const start = Math.floor(scrollTop / this.itemHeight)
// 取得可见区域的结束数据索引
+ const end = start + count + bufferCount
// 计算出可见区域对应的数据 ,让 Vue.js 更新
this.virtualRenderData = this.list.slice(start, end)
this.$refs.contentRef.style.webkitTransform = `translate3d(0, ${start * this.itemHeight}px, 0)`
})
}
3.2 完整代码和演示地址
定高虚拟滚动演示地址:atdow.github.io/learning-co…
定高虚拟滚动代码地址:github.com/atdow/learn…
4. 非定高虚拟滚动
4.1 封装思路
看了上面的定高虚拟滚动 ,我们对虚拟滚动技术已经有了基本的了解 。对于非定高虚拟滚动,需要解决的最大问题就是每一条需要渲染的数据的高度是不确定 ,这样我们就很难确定一屏需要渲染多少条数据 。
为了确定一屏需要渲染多少条数据 ,我们需要假设每条需要渲染数据的高度为一个假设值estimatedItemHeight=40,定义一个用于存储每一条渲染数据高度的数组itemHeightCache=[] ,定义一个用于存储每一条渲染数据距离顶部距离的数组itemTopCache=[](用于提升性能用 ,后面会做解释) ,以及定义撑开滚动条滚动容器高度的变量scrollBarHeight 。
假设我们我们的需要的数据list如下:
const list = [
{id:1,name:1},
{id:2,name:3},
....
]
我们先初始化itemHeightCache 、itemTopCache和scrollBarHeight:
const estimatedTotalHeight = this.list.reduce((pre, current, index) => {
// 给每一项一个虚拟高度
this.itemHeightCache[index] = { isEstimated: true, height: this.estimatedItemHeight }
// 给每一项距顶部的虚拟高度
this.itemTopCache[index] = index === 0 ? 0 : this.itemTopCache[index - 1] + this.estimatedItemHeight
return pre + this.estimatedItemHeight
}, 0)
// 列表总高
this.scrollBarHeight = estimatedTotalHeight
有了上面的初始化数据 ,我们就可以进行第一次假设渲染了:
// 更新数据函数
const update = function() {
const startIndex = this.getStartIndex()
// 如果是奇数开始 ,就取其前一位偶数
if (startIndex % 2 !== 0) {
this.startIndex = startIndex - 1
} else {
this.startIndex = startIndex
}
this.endIndex = this.getEndIndex()
this.visibleList = this.list.slice(this.startIndex, this.endIndex)
// 移动渲染区域
if (this.$refs.contentRef) {
this.$refs.contentRef.style.webkitTransform = `translate3d(0, ${this.itemTopCache[this.startIndex]}px, 0)`
}
}
// 获取开始索引
cont getStartIndex = function() {
const scrollTop = this.scrollTop
// 每一项距顶部的距离
const arr = this.itemTopCache
let index = -1
let left = 0,
right = arr.length - 1,
mid = Math.floor((left + right) / 2)
// 判断 有可循环项时进入
while (right - left > 1) {
/*
二分法:拿每一次获得到的 距顶部距离 scrollTop 同 获得到的模拟每个列表据顶部的距离作比较 。
arr[mid] 为虚拟列高度的中间项
不断while 循环 ,利用二分之一将数组分割 ,减小搜索范围
直到最终定位到 目标index 值
*/
// 目标数在左侧
if (scrollTop < arr[mid]) {
right = mid
mid = Math.floor((left + right) / 2)
} else if (scrollTop > arr[mid]) {
// 目标数在右侧
left = mid
mid = Math.floor((left + right) / 2)
} else {
index = mid
return index
}
}
index = left
return index
}
// 获取结束索引
const getEndIndex = function() {
const clientHeight = this.$refs.scrollbarRef?.clientHeight //渲染容器高度
let itemHeightTotal = 0
let endIndex = 0
for (let i = this.startIndex; i < this.dataList.length; i++) {
if (itemHeightTotal < clientHeight) {
itemHeightTotal += this.itemHeightCache[i].height
endIndex = i
} else {
break
}
}
endIndex = endIndex
return endIndex
}
update函数是用来更新需要渲染的数据的 ,核心逻辑就是获取截取数据的开始索引getStartIndex和结束索引getEndIndex以及移动被渲染数据容器。
当滚动条滚动的时候 ,我们就将scrollTop存起来,这个时候从itemTopCache中获取距离scrollTop最近的索引 ,就是我们需要截取数据的开始索引 。因为itemTopCache存储的就是每一条数据距离顶部的距离 ,所以直接取就行了,这也是为什么我们要先存储itemTopCache 。因为滚动的时候 ,我们都要从itemTopCache中使用二分法查找 ,不然就得从itemHeightCache中从头到尾一个一个遍历去对比查找,在数据量大的时候容易造成卡顿。
getEndIndex核心就是从itemHeightCache(存储每一条渲染数据高度的数组)中一条一条拿数据 ,从startIndex开始拿 ,一直拿到刚好填满渲染容器高度即可 ,就可以得到我们的截取数据的最后索引 (实际上这样是不够完美的 ,后面继续讲解) 。
移动被渲染数据容器的技巧和上面定高虚拟滚动类似 ,这里不做太多解释 。
在初始化完itemHeightCache 、itemTopCache和scrollBarHeight后 ,我们就可以手动调一次update函数进行第一次渲染了(this.update()) ,使用的都是预设的假定值。
在说更新之前 ,我们需要先定义一下子组件 ,也就是每一条被渲染数据的容器 。这样当数据被更新渲染之后(需要通知暴露index和height参数),就可以得到真实的dom的高度 ,通知我们去更新itemHeightCache、itemTopCache和scrollBarHeight ,更新逻辑如下:
const updateItemHeight = function({ index, height }) {
// 每次创建的时候都会抛出事件,因为没有处理异步的情况 ,所以必须每次高度变化都需要更新
// dom元素加载后得到实际高度 重新赋值回去
this.itemHeightCache[index] = { isEstimated: false, height: height }
// 重新确定列表的实际总高度
this.scrollBarHeight = this.itemHeightCache.reduce((pre, current) => {
return pre + current.height
}, 0)
// 更新itemTopCache
const newItemTopCache = [0]
for (let i = 1, l = this.itemHeightCache.length; i < l; i++) {
// 虚拟每项距顶部高度 + 实际每项高度
newItemTopCache[i] = this.itemTopCache[i - 1] + this.itemHeightCache[i - 1].height
}
// 获得每一项距顶部的实际高度
this.itemTopCache = newItemTopCache
}
dom更新完之后 ,初始化预定值计算出来需要的渲染数据就真的被渲染了,我们这个时候就可以再次调用update函数再次更新数据 ,自动更新弥补到渲染真实一屏需要渲染的数据了 。
const updateItemHeight = function({ index, height }) {
// 每次创建的时候都会抛出事件 ,因为没有处理异步的情况 ,所以必须每次高度变化都需要更新
// dom元素加载后得到实际高度 重新赋值回去
this.itemHeightCache[index] = { isEstimated: false, height: height }
// 重新确定列表的实际总高度
this.scrollBarHeight = this.itemHeightCache.reduce((pre, current) => {
return pre + current.height
}, 0)
// 更新itemTopCache
const newItemTopCache = [0]
for (let i = 1, l = this.itemHeightCache.length; i < l; i++) {
// 虚拟每项距顶部高度 + 实际每项高度
newItemTopCache[i] = this.itemTopCache[i - 1] + this.itemHeightCache[i - 1].height
}
// 获得每一项距顶部的实际高度
this.itemTopCache = newItemTopCache
+ this.update() // 自动更新
}
当滚动的时候 ,存储scrollTop ,手动调用update函数 ,将会自动更新 ,整个过程如下:
html结构如下:
<div class="virtual-list-dynamic-height" ref="scrollbarRef" @scroll="onScroll">
<div class="list-view-phantom" :style="{ height: scrollBarHeight + px }"></div>
<!-- 列表总高 -->
<ul ref="contentRef">
<Item
v-for="item in visibleList"
:data="item.data"
:index="item.index"
:key="item.index"
@update-height="updateItemHeight"
>
{{item}}
</Item>
</ul>
</div>
跟定高虚拟滚动不同点就是 ,需要定义子组件 ,同时传递给子组件index索引 。visibleList需要定义为[{index:xxx,data:xxx}]的数据格式,将index给储存起来 ,这样在子组件更新的时候才能获取到index 。
4.2 调优
在上面的代码中 ,基本可以实现基础的非定高虚拟滚动了,但是还是无法应对复杂的情况 。
我们举一个极端的例子:当一条数据的真实高度是200 ,其他数据的真实高度高度是10 ,渲染容器的高度是300 。在第一次假设渲染并且更新后我们的itemHeightCache 、itemTopCache和scrollBarHeight后,我们将会得到这样的结果 。渲染容器中渲染的是数据是第一条数据和剩下的9条数据 ,刚刚好渲染一屏数据 ,这样是没有任何问题的 。
当滚动条滚动的时候 ,我们滚动了20px的距离 ,获取到的startIndex应该是0 ,因为距离顶部最近的数据是第一条数据 ,这个就会造成下部空白20px的区域 。当滚动了80px的时候 ,获取到的startIndex也是0 ,原理同上 ,下部造成了空白区域将会是恐怖的80px。
为了解决空白局域,靠缓冲渲染bufferCount是不够的 ,就算bufferCount给了4 ,多四条数据也无法填充满空白区域 。调大bufferCount容易造成性能问题,也不能确定bufferCount到底给多少才能合适 。所以需要调整getEndIndex的逻辑 ,不再是从startIndex获取到刚好填充满渲染区域 ,而是从startIndex获取到刚好填充满渲染区域+statIndex的高度。这样无论startIndex的高度是多少,我们都能填充满整个渲染容器 ,因为空白区域最大高度就是startIndex的高度 。同时我们在endIndex上加上bufferCount ,就可以达到完美的效果 。
// 获取结束索引
const getEndIndex = function() {
+ const whiteHeight = this.scrollTop - this.itemTopCache[this.startIndex] // 出现留白的高度
const clientHeight = this.$refs.scrollbarRef?.clientHeight //渲染容器高度
let itemHeightTotal = 0
let endIndex = 0
for (let i = this.startIndex; i < this.dataList.length; i++) {
+ if (itemHeightTotal < clientHeight+whiteHeight) {
itemHeightTotal += this.itemHeightCache[i].height
endIndex = i
} else {
break
}
}
+ endIndex = endIndex + bufferCount
return endIndex
}
3.3 完整代码和演示地址
非定高虚拟滚动演示地址:atdow.github.io/learning-co…
非定高虚拟滚动代码地址:github.com/atdow/learn…
4 总结
有了非定高虚拟滚动组件 ,不就是可以应对各种情况了 ,为什么还需要做定高虚拟滚动组件?
在上面的封装思路中 ,我们能清晰知道非定高虚拟滚动组件是用假定值进行渲染的 ,在真实渲染过后才会弥补更新 ,而定高虚拟滚动所有东西都是确定的。所以定高虚拟滚动的优势就是比非定高虚拟滚动性能高 ,缺点就是只能应对每一条渲染数据是固定的情况 。
定高虚拟滚动:
优点:性能比非定高虚拟滚动高
缺点:只能应用于每一条渲染数据高度是固定的场景
非定高虚拟滚动:
优点:性能比定高虚拟滚动低
缺点:能应用于每一条渲染数据高度是动态的场景
本文转载于:
https://juejin.cn/post/7204450037031092283
如果对您有所帮助 ,欢迎您点个关注,我会定时更新技术文档 ,大家一起讨论学习 ,一起进步 。
声明:本站所有文章,如无特殊说明或标注 ,均为本站原创发布 。任何个人或组织 ,在未征得本站同意时,禁止复制 、盗用 、采集 、发布本站内容到任何网站 、书籍等各类媒体平台 。如若本站内容侵犯了原著者的合法权益 ,可联系我们进行处理 。