前言
最近在做一个新冠疫情相关的项目,其中包括一个针对高危人员进行预警的功能。
简单来说就是前端向后端一次性获取最近一个时段的所有高危人员信息,并以滚动循环播放的形式展示这些信息。
滚动循环功能并不难实现,只需要编写一个控制容器scrollTop
的定时器即可。
难点在于,一个时段的高危人员信息多达上万条,如果直接将它们渲染到页面上,必然会造成浏览器的卡顿甚至崩溃。
好在在内存中保存这些信息并没有什么压力,所以我们可以通过循环地截取部分信息,再进行展示来实现这一功能。
如果不想在内存中保存所有信息,也可以参考本文思路通过调用接口来获取信息片段,这种情况下你可能需要考虑接口异常所带来的的影响。
一、滚动循环播放组件
首先准备仅包含基本功能的滚动循环播放组件AutoScroll.vue
:
<template>
<div :style="{ height: `${height}px` }" class="outer" @mouseover="stopScroll" @mouseleave="startScroll">
<div ref="inner1">
<slot />
</div>
<div ref="inner2" />
</div>
</template>
<script>
export default {
props: {
// 设定容器高度
height: {
type: Number,
default: 0
}
},
mounted() {
this.getDomRef();
this.tryScroll();
},
updated() {
this.tryScroll();
},
methods: {
// 将dom元素的引用绑定到this上
getDomRef() {
this.outer = this.$el;
this.inner1 = this.$refs.inner1;
this.inner2 = this.$refs.inner2;
},
tryScroll() {
// 如果inner1的高度超过outer,则将其内容原样保存一份至inner2中,并开启滚动定时器
if (this.inner1.clientHeight > this.outer.clientHeight) {
this.inner2.innerHTML = this.inner1.innerHTML;
this.startScroll();
} else {
// 否则清空inner2中的内容,并停止滚动定时器
this.inner2.innerHTML = "";
this.stopScroll();
}
},
// 开启滚动定时器
startScroll() {
if (!this.autoScrollInterval) {
this.autoScrollInterval = setInterval(() => {
// 如果已经滚动到inner1的底部,则重置滚动的位置到inner1的头部
if (this.outer.scrollTop >= this.inner1.offsetHeight) {
this.outer.scrollTop = 0;
// 触发complete事件
this.$emit("complete");
} else {
// 否则向下滚动1个单位
this.outer.scrollTop += 1;
// 如果滚动了inner1一半的内容,则触发half事件
if (
this.outer.scrollTop === Math.ceil(this.inner1.offsetHeight / 2)
) {
this.$emit("half");
}
}
}, 50);
}
},
// 停止滚动定时器
stopScroll() {
clearInterval(this.autoScrollInterval);
this.autoScrollInterval = null;
}
}
};
</script>
<style scoped>
.outer {
overflow: hidden;
}
</style>
该组件接收一个height
以确定父元素的高度
该组件的模板包括最外层的outer
容器及其2个子节点inner1
与inner2
。其中inner1
包含一个插槽,供组件使用者传入需要滚动展示的内容。inner2
会在inner1
的高度超过outer
容器高度时,保存inner1内容的副本
,以实现视觉上的无缝循环。
二、思路
将传入inner1
的内容拆分成两部分:inner1A
和inner1B
。各部分的高度都不小于可视区域的高度。
inner2 为 inner1 的副本,同理。
每当滚动至
图1
位置时,可以更新inner1B
部分的内容,inner2B
也会相应改变。当 inner2A 的顶部部与容器顶部重合时,会归位到 图1 的状态
同理,每当滚动至图2
位置时,可以更新inner1A
部分的内容,inner2A
也会相应改变。
章节一中的AutoScroll
组件会在容器滚动至inner1
一半高度和归位时分别触发half
和complete
事件,可以在这两个事件触发时更新非可视区域的内容。
三、最终实现
<template>
<AutoScroll
:height="30 * partSize"
@complete="handleComplete"
@half="handleHalf"
>
<div class="item part-a" v-for="(item, index) in partA" :key="`a${index}`">
{{ item.seq }}
</div>
<div class="item part-b" v-for="(item, index) in partB" :key="`b${index}`">
{{ item.seq }}
</div>
</AutoScroll>
</template>
<script>
import AutoScroll from "./AutoScroll";
import fakeItems from "./fakeItems";
export default {
components: {
AutoScroll
},
data() {
return {
// 生产成10000条数据
items: fakeItems(10000),
// 每个part包含的数据条数
partSize: 3,
// partA在items中的开始索引
partAStartIndex: 0,
// partB在items中的开始索引
partBStartIndex: 0
};
},
computed: {
// partA中的数据
partA() {
if (this.items.length > this.partSize) {
return this.items
.concat(this.items.slice(0, this.partSize))
.slice(this.partAStartIndex, this.partAStartIndex + this.partSize);
} else {
return this.items;
}
},
// partB中的数据
partB() {
if (this.items.length > this.partSize) {
return this.items
.concat(this.items.slice(0, this.partSize))
.slice(this.partBStartIndex, this.partBStartIndex + this.partSize);
} else {
return [];
}
}
},
mounted() {
this.partAStartIndex = 0;
this.partBStartIndex = this.partAStartIndex + this.partSize;
},
methods: {
// 当滚动至一半的位置时,修改partAStartIndex,计算属性partA会自动更新内容
handleHalf() {
this.partAStartIndex =
(this.partBStartIndex + this.partSize) % this.items.length;
},
// 当滚动归位时,修改partBStartIndex,计算属性partB会自动更新内容
handleComplete() {
this.partBStartIndex =
(this.partAStartIndex + this.partSize) % this.items.length;
}
}
};
</script>
<style scoped>
.outer {
border: 1px solid red;
}
/* item 总高度为30 */
.item {
line-height: 28px;
border: 1px dashed blue;
}
</style>
fakeItems.js
export default (count = 0) => {
const items = [];
for (let i = 0; i < count; i++) {
items.push({
seq: i + 1,
// ... 其他字段
});
}
return items;
};
如果不想将完整数据保存在内存中,可在 complete 和 half 事件触发时分别异步请求 partB 和 partA 的数据