【业务场景】长列表的处理

长列表的处理

1. 什么是长列表

在前端开发中,经常会遇到列表展示,如果列表项的数量比较多,我们一般选择采用分页的方式来进行处理

但传统的前后翻页方式只适用于后台的管理系统中,而在用户端、尤其是在移动端,为了保障用户体验,往往不适合采用前后翻页。

所谓长列表,就是指这些数据量较大且无法使用分页方式来加载的列表。常见的场景有:订单列表、优惠券列表、评论区等等。

长列表会带来以下两方面的问题:

  1. 数据过多,主要是接口返回的数据过多,首次展示的等待时间较长,且数据不好处理
  2. DOM元素过多,导致页面渲染卡顿,页面中的操作卡顿。

2. 长列表的处理方案

2.1 下拉加载(无限滚动)

实际上就是懒加载的方式,一次只加载列表的一部分,等滚动到底部时,再加载列表的下一部分,相当于在垂直方向上的分页叠加功能。

这里用一个简单的小demo来说明下拉加载的实现原理。

首先简单说一下实现的思路:

  1. 渲染列表数据的div溢出的部分被隐藏掉
  2. 当向上或向下滚动到div的顶部或底部时,说明这些数据已经被浏览完毕了,需要加载新的数据,由于我们使用一个数组来维护所有的数据,所以实际上加载数据时只需要操作数组就行了

问题有两个:

  • 如何判断向上滚动还是向下滚动
  • 如何判断已经滚动到底部了

对于第一个问题,我们可以在每次滚动的时候,将这一次滚动的 scrollTop 记录下来作为 lastScroll,在下一次滚动时,将 lastScroll 与本次的 scrollTop 作比较,就可以得到滚动的方向了,接下来就只需要对两个方向的滚动分别进行处理就行了

对于第二个问题,我们可以看下面这张图:

在这里插入图片描述

scrollHeight 表示元素内容的真实高度,scrollTop 表示元素滚动的距离,而 clientHeight 则是元素内容在视口中展示的高度。当一个元素滚动到底部时,它们之间有这样的关系:scrollTop + clientHeight = scrollHeight

这样问题都解决了,接下来就可以动手实现了。

首先准备好渲染数据的容器:

<template><div class="custom-view"><div class="list" ref="scroll"><div class="list_item" v-for="item in dataList" :key="item.index"><div class="list_item_content"><span class="list_item_content_info">{{ item.content }}</span></div></div></div></div>
</template>

然后模拟一下我们实际开发中获取数据的方法:

getData() {setTimeout(() => {this.dataList = [...]}, 1000)
},

接下来,监听容器的滚动事件:

<div class="list" @scroll="handleScroll" ref="scroll">...
</div>

在处理函数中,我们首先需要拿到滚动元素,并判断其滚动的方向:

const el = this.$refs.scroll
// 判断滚动方向,1 --- 向上滚动,-1 --- 向下滚动, 0 --- 没有滚动
let scrollDirection = Math.sign(this.lastScroll - el.scrollTop)

然后对各滚动方向进行处理,这里只做向下滚动的处理:

  1. 判断是否滚动到底部
  2. 是则加载数据
  3. 记录滚动位置

最后的处理函数如下:

// 滚动
handleScroll() {// 获取滚动元素const el = this.$refs.scroll// 判断滚动方向,1 --- 向上滚动,-1 --- 向下滚动, 0 --- 没有滚动let scrollDirection = Math.sign(this.lastScroll - el.scrollTop)if (scrollDirection == -1) {// 此时向下滚动// 判断是否滚动到底部if(el.scrollHeight - el.scrollTop - el.clientHeight >= 20) return const _that = this// 加载数据setTimeout(() => {if (_that.dataList.length < 25) {_that.dataList.push(...[{index: 20 + _that.i,content: `这是第${20 + _that.i}条数据`}])} else {showMessage(_that, 'success', '所有数据已经加载完毕')}_that.i++}, 1000)}// 记录滚动位置this.lastScroll = el.scrollTop
}

当然,我们还可以为列表加上一个加载动画,这样看起来不会很突兀:

<div class="list" @scroll="handleScroll" ref="scroll"><div class="list_item" v-for="item in dataList" :key="item.index"><div class="list_item_content"><span class="list_item_content_info">{{ item.content }} </span></div></div><div class="loading_container" v-if="isBottom"><div class="loading"></div><span>加载中...</span></div><div class="default" v-else></div>
</div>

CSS

.loading_container {display: flex;justify-content: center;align-items: center;.loading {animation: spin 1s linear infinite;border: 4px solid #f3f3f3;border-top: 4px solid #02af95;border-radius: 50%;width: 20px;height: 20px;margin: 10px 10px;}@keyframes spin {0% {transform: rotate(0deg);}100% {transform: rotate(360deg);}}
}
.default {min-height: 40px;
}

为了实现加载动画,我们需要添加两个标识:

  • isBottom — 是否滚动到了底部
  • isLoading — 是否处于加载状态,处于加载状态时,滚动不做处理

接下来对我们的处理函数进行调整:

// 滚动
handleScroll() {// 获取滚动元素const el = this.$refs.scroll// 判断滚动方向,1 --- 向上滚动,-1 --- 向下滚动, 0 --- 没有滚动let scrollDirection = Math.sign(this.lastScroll - el.scrollTop)if (scrollDirection == -1) {// 此时向下滚动// 判断是否滚动到底部this.isBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 20const _that = this// 加载数据if (this.isBottom && !this.isLoading) {this.isLoading = truesetTimeout(() => {if (_that.dataList.length < 25) {_that.dataList.push(...[{index: 20 + _that.i,content: `这是第${20 + _that.i}条数据`}])} else {showMessage(_that, 'success', '所有数据已经加载完毕')}_that.i++_that.isBottom = false_that.isLoading = false}, 1000)}}// 记录滚动位置this.lastScroll = el.scrollTop
}

demo的效果如下:

在这里插入图片描述

这只是一个简单的demo,在功能和样式上都有很大的优化空间,如果要在实际项目中实现无限滚动加载数据的话,可以使用:

  • 各组件库中的无限滚动组件
  • vue-infinite-scroll 插件
2.2 虚拟列表

对于上面的下拉加载方式,前面也说了,相当于在垂直方向上的分页叠加功能。但其又与实际的分页有所不同,因为其是将新的数据插入到原有数据的后面,这样,随着加载数据越来越多,浏览器的回流与重绘时的开销会越来越大。

而为了解决这一问题,我们就可以使用虚拟列表。虚拟列表的核心思想就是在处理用户滚动时,只改变列表在可视区域的渲染部分,然后使用padding或者translate来让渲染的列表偏移到可视区域中,给用户平滑滚动的感觉。

要实现虚拟列表,有以下五个步骤:

  1. 获取长列表的数据,但不会一次性将所有列表数据全部直接渲染在页面上
  2. 截取长列表中的一部分数据用于填充我们预留好的可视区域
  3. 长列表的不可视部分,我们使用空白的占位进行填充
  4. 监听滚动事件,根据滚动的位置,动态地改变可视列表中的数据项
  5. 监听滚动事件,根据滚动的位置,动态改变空白填充的大小

即下图

在这里插入图片描述

但是,我们还需要考虑一个问题:列表项的每一项高度是固定的吗?

由此,我们可以分为两种情况进行讨论

2.2.1 列表项固定高度

首先我们需要准备好模板:

HTML

<!-- 最外层的可视区容器 -->
<div ref="list" class="infinite-list-container" @scroll="throttle()">
<!-- <div ref="list" class="infinite-list-container" @scroll="scrollEvent($event)"> --><!-- 中间的可滚动区域,z-index=-1,高度和真实列表相同,目的是使得外层出现相同的滚动条 --><divclass="infinite-list-phantom":style="{ height: listHeight + 'px' }"></div><!-- 最上层的可视区列表,数据和偏移距离随着滚动距离的变化而变化 --><div class="infinite-list" :style="{ transform: getTransform }"><divclass="infinite-list-item"v-for="item in visibleData":key="item.id":style="{ height: itemSize + 'px' }">{{ item.label }}</div></div>
</div>

CSS

.infinite-list-container {height: 100%;overflow: auto;position: relative;
}/* 这里内部的滚动区域和可视区域都需要使用绝对定位,并将滚动区域放置在最底层 */
.infinite-list-phantom {position: absolute;left: 0;top: 0;right: 0;z-index: -1;
}.infinite-list {left: 0;right: 0;top: 0;position: absolute;
}.infinite-list-item {line-height: 50px;text-align: center;color: #555;border: 1px solid #ccc;box-sizing: border-box;
}

当列表项固定时,计算方式比较简单,首先我们需要根据外层视口的大小,计算出可以渲染多少条数据 limit 以及内部可滚动区域的总高度:

// 列表的总高度,用于模拟滚动条!
listHeight() {return this.items.length * this.itemSize;
},
// 可视区列表的项数,即视口的高度除去 itemSize 并向上取整
visibleCount() {return Math.ceil(this.screenHeight / this.itemSize);
},

然后我们可以得到视口中列表数据的开始索引和结束索引:

this.start = 0;
// 初始化时,同样要多渲染一项,防止滚动时下方出现空白
this.end = this.start + this.visibleCount + 1;

这样我们就可以从列表数据中截取出可视区域中的列表数据:

// 获取可视区列表数据
visibleData() {return this.items.slice(this.start,Math.min(this.end, this.items.length));
},

接下来就可以监听滚动事件并做出相应的处理了:

scrollEvent() {console.log(111)// 获取当前滚动位置let scrollTop = this.$refs.list.scrollTop;// 更新开始索引,向下取整this.start = Math.floor(scrollTop / this.itemSize);// 更新结束索引,这里多渲染一项,防止滚动时下方出现空白this.end = this.start + this.visibleCount + 1;// 此时的可视区列表向下偏移的距离/* 在滚动时,最上方的一项尚未离开视口,所以这个偏移距离需要减去多余的滚动距离 rest当最上方一项尚未离开视口时,rest 随着 scrollTop 变大但始终小于 itemSize,所以 scrollTop - rest 的结果是不会变化的,即这个偏移距离是不会变化的,也就是说这个偏移距离永远是 itemSize 的 n 倍!而 n 就是以及被滚动到视口上方的数据项数量当最上方一项刚好离开视口时,rest 为0,此时 scrollTop 就是当前滚动的距离,而 startOffset = scrollTop,相比之前,startOffset 增加了一个 itemSize 的大小。即,我们向下滚动一项,偏移就增大一项,从而实现视口跟随我们滚动的效果!*/this.startOffset = scrollTop - (scrollTop % this.itemSize);
},

这里我们最终记录了可视区域应该向下偏移的距离 startOffset

此时只需要为中间的可视区域加上偏移即可:

// 可视区列表偏移距离对应的样式,这里用 translate3d 来实现向下的偏移(实际上就是y轴正方向上的偏移)
getTransform() {return `translate3d(0,${this.startOffset}px,0)`;
},

这样就完成了一个简单的固定高度虚拟列表,效果如下:

在这里插入图片描述

可以看到,成功模拟了列表的滚动效果,同时,页面中仅仅只渲染了当前可视区中的7个节点

完整代码如下:

myVirtualScroller.vue

<template><!-- 最外层的可视区容器 --><div ref="list" class="infinite-list-container" @scroll="throttle()"><!-- <div ref="list" class="infinite-list-container" @scroll="scrollEvent($event)"> --><!-- 中间的可滚动区域,z-index=-1,高度和真实列表相同,目的是使得外层出现相同的滚动条 --><divclass="infinite-list-phantom":style="{ height: listHeight + 'px' }"></div><!-- 最上层的可视区列表,数据和偏移距离随着滚动距离的变化而变化 --><div class="infinite-list" :style="{ transform: getTransform }"><divclass="infinite-list-item"v-for="item in visibleData":key="item.id":style="{ height: itemSize + 'px' }">{{ item.label }}</div></div></div>
</template><script>
export default {name: "MyVirtualList",props: {//列表数据items: {type: Array,default: () => [],},//列表项高度itemSize: {type: Number,default: 50,},},computed: {// 列表的总高度,用于模拟滚动条!listHeight() {return this.items.length * this.itemSize;},// 可视区列表的项数,即视口的高度除去 itemSize 并向上取整visibleCount() {return Math.ceil(this.screenHeight / this.itemSize);},// 可视区列表偏移距离对应的样式,这里用 translate3d 来实现向下的偏移(实际上就是y轴正方向上的偏移)getTransform() {return `translate3d(0,${this.startOffset}px,0)`;},// 获取可视区列表数据visibleData() {return this.items.slice(this.start,Math.min(this.end, this.items.length));},},mounted() {// 初始化时,获取可视窗口的高度,用于计算出当前可视窗口中可以渲染几项数据this.screenHeight = this.$refs.list.clientHeight;this.start = 0;// 初始化时,同样要多渲染一项,防止滚动时下方出现空白this.end = this.start + this.visibleCount + 1;},data() {return {screenHeight: 0, //可视区域高度startOffset: 0, //偏移距离start: 0, //起始索引end: 0, //结束索引// 上一次触发的时间lastTime: 0,};},methods: {scrollEvent() {console.log(111)// 获取当前滚动位置let scrollTop = this.$refs.list.scrollTop;// 更新开始索引,向下取整this.start = Math.floor(scrollTop / this.itemSize);// 更新结束索引,这里多渲染一项,防止滚动时下方出现空白this.end = this.start + this.visibleCount + 1;// 此时的可视区列表向下偏移的距离/* 在滚动时,最上方的一项尚未离开视口,所以这个偏移距离需要减去多余的滚动距离 rest当最上方一项尚未离开视口时,rest 随着 scrollTop 变大但始终小于 itemSize,所以 scrollTop - rest 的结果是不会变化的,即这个偏移距离是不会变化的,也就是说这个偏移距离永远是 itemSize 的 n 倍!而 n 就是以及被滚动到视口上方的数据项数量当最上方一项刚好离开视口时,rest 为0,此时 scrollTop 就是当前滚动的距离,而 startOffset = scrollTop,相比之前,startOffset 增加了一个 itemSize 的大小。即,我们向下滚动一项,偏移就增大一项,从而实现视口跟随我们滚动的效果!*/this.startOffset = scrollTop - (scrollTop % this.itemSize);},// 节流函数throttle() {const now = Date.now()// 这里设置的间隔时间一般为 30ms,如果再设置大一点,列表的底部就会出现空白if (now - this.lastTime > 30) {this.lastTime = now// 与 setInterval 不同,window.requestAnimationFrame 不需要指定执行的间隔时间,而是会在浏览器下一次重绘之前执行// 至于真正执行的时机,是由我们屏幕的刷新率来决定的/* 由于这里我们的处理函数中触发了浏览器的重绘,所以使用 window.requestAnimationFrame 相比 setInterval 更有优势因为 requestAnimationFrame 会把每一帧中的所有DOM操作集中起来,在一次重绘或回流中就完成,这样看起来更加平滑。同时对于 setInterval 而言,由于其在任务队列中会被阻塞,所以实际上每次等待的时间可能会大于我们指定的时间,但 requestAnimationFrame 可以保证在每一帧中都执行回调另外,使用 requestAnimationFrame 也有助于性能的提升*/window.requestAnimationFrame(() => this.scrollEvent())}},},/* 存在的问题:滑动过快时仍会出现白屏。*/
};
</script><style scoped>
.infinite-list-container {height: 100%;overflow: auto;position: relative;
}.infinite-list-phantom {position: absolute;left: 0;top: 0;right: 0;z-index: -1;
}.infinite-list {left: 0;right: 0;top: 0;position: absolute;
}.infinite-list-item {line-height: 50px;text-align: center;color: #555;border: 1px solid #ccc;box-sizing: border-box;
}
</style>

App.vue

<template><div class="container"><my-virtual-scroller :items="list" /></div>
</template><script>
import myVirtualScroller from "@/components/myVirtualScroller";
// 模拟一个长列表
const list = [];
for (let i = 0; i < 10000; i++) {list.push({id: i,label: `virtual-list ${i}`,});
}
export default {components: {myVirtualScroller,},data() {return {list: list,};},
};
</script><style scoped>
.container {height: 300px;border: 1px solid #ccc;
}
</style>

当然,这里也存在一个问题,当我们滚动的速度较快时,会出现白屏的现象。

2.2.2 列表项高度不固定

在列表项高度固定时,有很多相关的属性计算都很简单:

  1. 内部滚动区域的总高度 listHeight
  2. 可视窗口偏移量 startOffset
  3. 开始结束索引

但当列表项的高度不固定时,我们该如何计算这些属性呢?要计算这些属性,我们首先至少需要拿到列表项的真实高度,如何拿到?

所以,我们现在有下面几个问题需要解决:

  1. 如何获取列表项的真实高度?
  2. 如何计算相关属性?
  3. 如何渲染?
1. 列表项的真实高度

在实际渲染列表项的内容之前,我们是无从得知列表项的真实高度的,所以我们只能先预估一个高度,等待渲染出真实DOM后,在根据DOM的具体情况来设置高度

最后,我们还需要准备一个数组,将列表项的索引、高度以及定位存放在里面,初始化时,用我们预估的高度来初始化数组,在渲染出真实DOM后,再来更新这个数组。

2. 如何计算相关属性

既然列表项的高度不是固定的,那么我们原本的计算逻辑就都不能使用了,需要根据我们维护的数组来进行调整

3. 列表的渲染

具体的渲染方式不用进行调整,但开始索引的计算逻辑需要修改,现在我们需要在缓存列表中搜索第一个底部定位大于列表垂直偏移量的项并返回它的索引作为开始索引。


接下来就是具体实现了

首先要拿到列表的数据,并为列表项预估一个高度:

props: {//所有列表数据listData: {type: Array,default: () => [],},//预估高度estimatedItemSize: {type: Number,required: true,},//容器高度 100px or 50vhheight: {type: String,default: "100%",},
},

然后先将列表数据处理一下,把列表数据的索引单独拿出来存进去,同时根据我们预估的高度,先算出一个大概的可视区域中可渲染列表项数量:

computed: {// 处理列表数据,为其加上一个自带的索引_listData() {return this.listData.map((item, index) => {return {_index: `_${index}`,item,};});},// 可视区域的可渲染列表项数量visibleCount() {return Math.ceil(this.screenHeight / this.estimatedItemSize);},...,
}

接下来,准备一个 positions 数组,用于存放列表项的索引、高度以及定位信息,并在组件创建时,用我们预估的高度来初始化这个数组:

data() {return {...,positions: [],...,}
},
method: {// 初始化 positions 数组initPositions() {this.positions = this.listData.map((d, index) => ({index,height: this.estimatedItemSize, // 用预估高度来初始化top: index * this.estimatedItemSize,bottom: (index + 1) * this.estimatedItemSize,}));},...,
},
created() {this.initPositions();
},

接下来,我们需要在组件挂载时,初始化我们的视口大小,以及可视区域列表数据的开始索引和结束索引

mounted() {this.screenHeight = this.$el.clientHeight;this.start = 0;// 这里我们更新了 screenHeight 后,会触发 visibleCount 重新计算,所以我们这里直接用开始索引加上可视区域中的列表项数量即可// 这里可能会多渲染一两项,但是为了避免下方的白屏问题本来就需要多渲染几项,所以正好this.end = this.start + this.visibleCount;
},

有了开始索引和结束索引,我们就可以从列表数据中截取出可视区域中的列表数据:

computed: {...,// 可视区域中的列表项visibleData() {return this._listData.slice(this.start, this.end);},
}

下一步,准备好数据展示的容器:

HTML

<!-- 最外层的可视区容器 -->
<divref="list":style="{ height }"class="infinite-list-container"@scroll="scrollEvent($event)"><!-- 中间的可滚动区域,z-index=-1,高度和真实列表相同,目的是使得外层出现相同的滚动条 --><div ref="phantom" class="infinite-list-phantom"></div><!-- 最上层的可视区列表,数据和偏移距离随着滚动距离的变化而变化 --><div ref="content" class="infinite-list"><divclass="infinite-list-item"ref="items":id="item._index":key="item._index"v-for="item in visibleData"><p><span style="color: red">{{ item.item.id }}</span>&nbsp;<span style="color: blue">{{ item.item.value }}</span></p></div></div>
</div>

CSS

.infinite-list-container {overflow: auto;position: relative;-webkit-overflow-scrolling: touch;
}.infinite-list-phantom {position: absolute;left: 0;top: 0;right: 0;z-index: -1;
}.infinite-list {left: 0;right: 0;top: 0;position: absolute;
}.infinite-list-item {padding: 5px;color: #555;box-sizing: border-box;border-bottom: 1px solid #999;/* height:200px; */
}

其实展示的逻辑与前面的固定高度虚拟列表相比并不需要变化。

接下来的问题,就是需要在渲染后,拿到真实DOM的高度并更新我们的 positions 数组:

// 获取列表项的当前尺寸
updateItemsSize() {// 拿到当前可视区域中渲染的节点 NodeListlet nodes = this.$refs.items;nodes.forEach((node) => {// 获取该节点相对于视口的上下左右的位置以及自身的宽高信息let rect = node.getBoundingClientRect();let height = rect.height;// 拿到节点的id,实际上就是我们列表项的索引,只不过要从字符串转为numberlet index = +node.id.slice(1);// 节点原本的高度let oldHeight = this.positions[index].height;// 计算出差值let dValue = oldHeight - height;// 如果存在差值if (dValue) {/* 更新该节点本身的定位信息:1. 根据差值更新该节点底部距离滚动区域顶部的距离2. 更新该节点的高度信息*/this.positions[index].bottom = this.positions[index].bottom - dValue;this.positions[index].height = height;// 根据更新后的信息,将该节点后续的所有列表项的信息也进行相应的修改for (let k = index + 1; k < this.positions.length; k++) {// 直接拿前一项的 bottom 作为这一项的 topthis.positions[k].top = this.positions[k - 1].bottom;// 这一项的 bottom 就直接减去刚刚的差值即可this.positions[k].bottom = this.positions[k].bottom - dValue;}}});
},

那么我们在哪里调用这个方法呢?

注意,当我们滚动时,我们会更新DOM以及相关的数据,但上面这些 positons 中的数据,并不是每一次滚动时都需要修改,而是当DOM发生变化时,才需要更新!

所以,我们不能在滚动的处理函数中调用该方法,因为这样会有多余的调用。

这里我们选择在 updated 生命周期中调用该方法,即组件DOM或其中的数据更新时,才触发 positions 中数据的更新!但同时,我们还需要根据最新的 positions 数组中的数据来更新列表的总高度并重新计算可视区域的偏移量:

updated() {this.$nextTick(function () {if (!this.$refs.items || !this.$refs.items.length) {return;}// 获取当前可视区域中真实元素大小,修改对应的尺寸缓存this.updateItemsSize();// 更新列表总高度,用列表的最后一项的 bottom 属性,即列表最后一项底部距离滚动区域顶部的距离,来作为列表的总高度let height = this.positions[this.positions.length - 1].bottom;this.$refs.phantom.style.height = height + "px";// 更新真实偏移量this.setStartOffset();});
},

接下来,就是需要监听滚动事件并更新可视区域的列表数据了

但是,我们还需要准备一个更新开始索引的方法,以及最后重新计算可视区域偏移的方法:

/* 获取列表起始索引,由于我们在 positions 数组中存放的数据是有序的且我们计算起始索引的方式是:将 positions 数组中 bottom 属性与已滚动距离 scrollTop 相等的列表项的下一项作为起始项所以可以使用二分查找的方法来获取起始索引
*/
getStartIndex(scrollTop = 0) {// 二分法查找return this.binarySearch(this.positions, scrollTop);
},
// 二分法查找
binarySearch(list, value) {let start = 0;let end = list.length - 1;let tempIndex = null;while (start <= end) {let midIndex = parseInt((start + end) / 2);let midValue = list[midIndex].bottom;if (midValue === value) {return midIndex + 1;} else if (midValue < value) {start = midIndex + 1;} else if (midValue > value) {if (tempIndex === null || tempIndex > midIndex) {tempIndex = midIndex;}end = midIndex - 1;}}return tempIndex;
},// 获取当前的偏移量
setStartOffset() {// 将开始索引的前一项列表项的 bottom 属性,即距离滚动区域顶部的距离,作为当前的偏移量let startOffset =this.start >= 1 ? this.positions[this.start - 1].bottom : 0;// 仍然使用 translate3d 实现偏移this.$refs.content.style.transform = `translate3d(0,${startOffset}px,0)`;
},

最后,我们监听外层容器的滚动事件,并更新开始索引和结束索引,以及偏移量即可:

// 滚动事件
scrollEvent() {// 当前滚动位置let scrollTop = this.$refs.list.scrollTop;// 获取开始索引this.start = this.getStartIndex(scrollTop);// 获取结束索引this.end = this.start + this.visibleCount;// 更新偏移量this.setStartOffset();
},

这样就实现了一个简单的不固定高度的虚拟列表,效果如下:
在这里插入图片描述

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

最后最后,我们来分析一下滚动的过程:

随着我们的滚动,当可视区域中的第一项尚未离开可视区域时,开始索引并不会发生变化

为什么?

因为我们是用 positions 中 【bottom 等于此时 scrollTop 的列表项的下一项】或【bottom 大于 scrollTop的列表项】 作为起始项,而此时没有满足第一个条件的列表项,且第一项的 bottom 仍然大于 scrollTop

既然开始索引没有变化,则结束索引也不会变化,那么可视区域中渲染的列表项也没有变化,所有 positions 中的高度、定位信息并没有变化,因此,startOffset 也不会变化,即可视区域在滚动区域中的位置不会变化,从而达到列表向上滚动的效果

而一旦第一项离开可视区域,开始索引变化,引起结束索引变化,进而引发 positions 中高度、定位信息的更新,最终导致 startOffset 变化,使得可视区域向下进一步偏移。

这两者效果结合,就模拟出了列表滚动的效果。

但是,对于高度不固定的虚拟列表,存在以下三个问题:

  1. 滚动过快时,会出现白屏
  2. 由于我们估计可视区域中可展示的列表项数量时,是根据我们预估的高度来计算的,如果我们预估的高度比实际高度高出太多,会导致可视区域中渲染的列表项数量过少,导致占不满可视区域的问题
  3. 如果列表项中需要展示图片,由于渲染时图片可能未加载出来,会导致计算高度时不准确

当然,实际开发中我们肯定不会专门为一个虚拟列表写这么多代码,与无限滚动相同,虚拟列表也有成熟的插件可供我们使用:

  • vue-virtual-scroller
2.3 虚拟列表中白屏问题的解决
2.4 分页 + 虚拟列表

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/192001.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

基于51单片机PCF8591数字电压表数码管显示设计( proteus仿真+程序+设计报告+讲解视频)

PCF8591数字电压表数码管显示 1.主要功能&#xff1a;讲解视频&#xff1a;2.仿真3. 程序代码4. 设计报告5. 设计资料内容清单&&下载链接资料下载链接&#xff08;可点击&#xff09;&#xff1a; 基于51单片机PCF8591数字电压表数码管设计( proteus仿真程序设计报告讲…

kafka和rocketMq的区别

kafka topic 中每一个分区会有 Leader 与 Follow。Kafka 的内部机制可以保证 topic 某一个分区的 Leader 与 Follow 不在同一台机器上 Leader 节点承担一个分区的读写&#xff0c;Follow 节点只负责数据备份 如果 Leader 分区所在的 Broker 节点宕机&#xff0c;会触发主从节…

【C/C++底层】内存分配:栈区(Stack)与堆区(Heap)

/*** poject * author jUicE_g2R(qq:3406291309)* file 底层内存分配&#xff1a;栈区(Stack)与堆区(Heap)* * language C/C* EDA Base on MVS2022* editor Obsidian&#xff08;黑曜石笔记软件&#xff09;* * copyright 2023* COPYRIGHT …

Linux应用层点亮硬件的LED灯

一 应用层操作硬件的两种方法 应用层想要对底层硬件进行操控&#xff0c;通常可以通过两种方式&#xff1a; /dev/目录下的设备文件&#xff08;设备节点&#xff09;&#xff1b;/sys/目录下设备的属性文件。 具体使用哪种方式需要根据不同功能类型设备进行选择&#xff0c;通…

保序回归:拯救你的校准曲线(APP)

保序回归&#xff1a;拯救你的校准曲线&#xff08;APP&#xff09; 校准曲线之所以是评价模型效能的重要指标是因为&#xff0c;校准曲线衡量模型预测概率与实际发生概率之间的一致性&#xff0c;它可以帮助我们了解模型的预测结果是否可信。一个理想的模型应该能够准确地预测…

Python 框架学习 Django篇 (十) Redis 缓存

开发服务器系统的时候&#xff0c;程序的性能是至关重要的。经过我们前面框架的学习&#xff0c;得知一个请求的处理基本分为接受http请求、数据库处理、返回json数据&#xff0c;而这3个部分中就属链接数据库请求的响应速度最慢&#xff0c;因为数据库操作涉及到数据库服务处理…

深入了解springmvc响应数据

目录 一、前后端分离开发与混合开发 1.1 混合开发模式 1.2 前后端分离模式【重点】 二、页面跳转控制 2.1 通过JSP实现页面跳转 2.2 转发与重定向 三、返回JSON数据 3.1 导包与配置 3.2 使用ResponseBody 四、返回静态资源 4.1 为什么无法直接查询静态资源 4.2 配置…

vscode 访问本地或者远程docker环境

1、vscode 访问docker本地环境 直接点击左下角连接图标&#xff0c;弹出选项可以选择容器&#xff0c;只要容器在本地运行者&#xff0c;选择attach可以看到运行中的容器可以选择&#xff0c;选择其中需要选择的就行 ## 运行容器&#xff0c;可以-d后台运行都可以 docker run…

hadoop 虚拟机配置大数据环境 hadoop(二)

1. 安装epel-release 注:Extra Packages for Enterprise Linux 是为“红帽系”的操作系统提供额外的软件包,适用于RHEL、CentOS 和Scientific Liux。相当于是一个软件仓库&#xff0c;大多数rpm 包在官方repository 中是找不到的) 命令&#xff1a; yum install -y epel-relea…

SpringBoot3基础特性

SpringBoot3基础特性 SpringApplication 自定义banner 类路径添加banner.txt或设置spring.banner.location就可以定制banner推荐网站:Spring Boot banner在线生成工具&#xff0c;制作下载英文banner.txt,修改替换banner.txt文字实现自定义。 提示&#xff1a; 可以通过修改配…

为什么数据安全很重要?哪些措施保护数据安全?

数据安全很重要的原因是因为数据是现代社会的重要财产之一。很多组织和企业依赖数据来做出商业决策&#xff0c;管理客户关系&#xff0c;进行财务规划等等。如果这些数据泄露或遭到黑客攻击&#xff0c;那么就会影响企业的经济利益&#xff0c;甚至影响到个人的隐私和安全。此…

VuePress介绍及使用指南

VuePress是一个基于Vue.js的静态网站生成工具&#xff0c;它专注于以Markdown为中心的项目文档。VuePress具有简单易用的特性&#xff0c;同时提供了强大的自定义和扩展性。在本文中&#xff0c;我们将介绍VuePress的基本概念&#xff0c;并提供一个简单的使用指南。 什么是Vue…

Linux shell编程学习笔记25:tty

1 tty的由来 在 1830 年代和 1840 年代&#xff0c;开发了称为电传打字机&#xff08;teletypewriters&#xff09;的机器&#xff0c;这些机器可以将发件人在键盘上输入的消息“沿着线路”发送在接收端并打印在纸上。 电传打字机的名称由teletypewriters&#xff0c; 缩短为…

计算机毕业设计 基于SpringBoot的在线考试系统的研究与实现 Java实战项目 附源码+文档+视频讲解

博主介绍&#xff1a;✌从事软件开发10年之余&#xff0c;专注于Java技术领域、Python人工智能及数据挖掘、小程序项目开发和Android项目开发等。CSDN、掘金、华为云、InfoQ、阿里云等平台优质作者✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精…

[Hive] INSERT OVERWRITE DIRECTORY要注意的问题

在使用Hive的INSERT OVERWRITE语句时&#xff0c;需要注意以下问题&#xff1a; 数据覆盖&#xff1a;INSERT OVERWRITE语句会覆盖目标目录中的数据。因此&#xff0c;在执行该语句之前&#xff0c;请确保目标目录为空或者你希望覆盖的数据已经不再需要。数据格式&#xff1a;…

香港科技大学广州|智能制造学域机器人与自主系统学域博士招生宣讲会—中国科学技术大学专场

&#x1f3e0;地点&#xff1a;中国科学技术大学西区学生活动中心&#xff08;一楼&#xff09;报告厅 【宣讲会专场1】让制造更高效、更智能、更可持续—智能制造学域 &#x1f559;时间&#xff1a;2023年11月16日&#xff08;星期四&#xff09;18:00 报名链接&#xff1a…

Django视图层()

视图层 django视图层&#xff1a;Django项目下的views.py文件&#xff0c;它的内部是一系列的函数或者是类,用来处理客户端的请求后处理并返回相应的数据 三板斧 HttpResponse # 返回字符串 render # 返回html页面&#xff0c;并且在返回浏览器之前还可以给html文件…

matlab 二自由度操纵稳定性汽车模型

1、内容简介 略 19-可以交流、咨询、答疑 二自由度操纵稳定性汽车模型 二自由度、操纵稳定性、操纵动力学 2、内容说明 1 模型假设 忽略转向系的影响&#xff0c;以前、后轮转角作为输入&#xff1b;汽车只进行平行于地面的平面运动&#xff0c;而忽略悬架的作用&#xf…

linux时间同步

搭建集群时&#xff0c;都会先设置时间同步&#xff0c;否则会出现多种问题。 方式一&#xff1a; 1.安装ntp软件 yum install -y ntp 2.更新时区 删除原有时区&#xff1a;sudo rm -f /etc/localtime 加载新时区&#xff1a;sudo ln -s /usr/share/zoneinfo/Asia/Shangh…

H5游戏源码分享-超级染色体小游戏

H5游戏源码分享-超级染色体小游戏 游戏玩法 不断地扩大发展同颜色的色块 用最少的步数完成游戏 <!DOCTYPE html> <html><head><meta charset"UTF-8"><meta name"viewport"content"widthdevice-width,user-scalableno,init…