首页IT科技模拟滚动(记录–虚拟滚动探索与封装)

模拟滚动(记录–虚拟滚动探索与封装)

时间2025-06-15 03:22:36分类IT科技浏览7484
导读:这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助...

这里给大家分享我在网上总结出来的一些知识              ,希望对大家有所帮助

<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

如果对您有所帮助              ,欢迎您点个关注,我会定时更新技术文档                     ,大家一起讨论学习                     ,一起进步                     。

声明:本站所有文章,如无特殊说明或标注              ,均为本站原创发布       。任何个人或组织                     ,在未征得本站同意时       ,禁止复制                     、盗用       、采集       、发布本站内容到任何网站                     、书籍等各类媒体平台       。如若本站内容侵犯了原著者的合法权益              ,可联系我们进行处理                     。

创心域SEO版权声明:以上内容作者已申请原创保护,未经允许不得转载,侵权必究!授权事宜、对本内容有异议或投诉,敬请联系网站管理员,我们将尽快回复您,谢谢合作!

展开全文READ MORE
静态还是动态?WEB前端页面该怎么选择(在不同需求下的优缺点分析) gateway静态资源(vite中静态资源(css、img、svg等)的加载机制及其相关配置)