HTML元素
<div class="fs-estimated-virtuallist-container">
<div
class="fs-estimated-virtuallist-content"
ref="contentRef"
@mouseleave="leave"
@mouseenter="enter"
@mouseover="over"
@scroll="handleScroll"
>
<div class="fs-estimated-virtuallist-list" ref="listRef" :style="scrollStyle">
<div class="fs-estimated-virtuallist-list-item" ref="itemRef" v-for="i in renderList" :key="i.project_id">
<slot name="item" :item="i"></slot>
</div>
</div>
<div class="fs-estimated-virtuallist-list" :style="scrollStyle2">
<div class="fs-estimated-virtuallist-list-item" v-for="i in virList" :key="i.project_id">
<slot name="item" :item="i"></slot>
</div>
</div>
</div>
</div>
监听容器高度变化
1.使用ResizeObserver监听容器高度变化
const observeContent = () => {
if (contentRef.value) {
const observer = new ResizeObserver(debounce(handleSetPosition, 300));
observer.observe(contentRef.value);
}
};
2.动态计算尺寸 maxCount和itemHeight的高度
if (itemRef.value) {
// 拿数组第一项计算最大 也就是计算item的高度可以放下多少个item
state.maxCount = Math.ceil(state.viewHeight / itemRef.value[0].offsetHeight) + 1;
state.itemHeight = itemRef.value[0].offsetHeight;
}
4.虚拟滚动(核心)
startIndex默认为0
virList用于无缝滚动
// dada 的长度和 maxCount 最大长度 取最小的
const endIndex = computed(() => Math.min(propList.value.length, state.startIndex + state.maxCount));
// 截取的长度是 当前滚动到的索引到当前容器可视区域的最大索引
const renderList = computed(() => propList.value.slice(state.startIndex, endIndex.value));
const virList = computed(() => propList.value.slice(0, state.maxCount)); // 复制多几个用来无缝滚动的
当页面滚动时 重新计算startIndex值
state.startIndex = Math.floor(scrollTop / state.itemHeight);
当页面滚动到底部时contentRef.value!.scrollTop = 0;
const handleScroll = rafThrottle(() => {
let { scrollTop } = contentRef.value!;
// 计算启示索引 容器滚动的高度/单个数据的高度
// 获取到当前滚动到的索引
state.startIndex = Math.floor(scrollTop / state.itemHeight);
if (renderList.value.length === 0) {
contentRef.value!.scrollTop = 0;
!props.loading && emit('scroll-end');
}
});
5.无缝滚动 在move()函数中 通过不断修改scrollTop触发滚动
<hr>
// 复制多几个用来无缝滚动的
const scrollStyle = computed(
() =>
({
height: `${state.itemHeight * renderList.value.length}px`,
transform: `translate3d(0, ${state.itemHeight * state.startIndex}px, 0)`
} as CSSProperties)
);
const scrollStyle2 = computed(
() =>
({
transform: `translate3d(0, ${state.itemHeight * state.startIndex}px, 0)`
} as CSSProperties)
);
当 scrollTop 超过第一个列表的高度时,第二个列表会通过 translate3d 的位移“顶替”第一个列表的位置。由于两个列表的内容相同,用户会感觉内容无限循环。
6.丝滑滚动
使用requestAnimationFrame
实现高性能滚动事件 代替setTimeout
他的执行次数和屏幕的刷新率相当 比如屏幕是75Hz就是1秒执行75次
原因:
- 1、动画更丝滑,不会出现卡顿
- 2、性能更好,切后台会暂停
确保只有在存在有效的动画帧请求 ID 时,才尝试取消它
终止APIwindow.cancelAnimationFrame()
//自动滚动
const move = () => {
if (state.isHover) return;
// 加载中就停止
if (props.loading) {
_cancel();
return;
}
contentRef.value!.scrollTop += 1;
state.rafTimer = requestAnimationFrame(move);
// setTimeout(() => {
// move();
// }, 50);
};
const enter = () => {
state.isHover = true; //关闭_move
_cancel();
};
//取消滚动
const _cancel = () => {
state.rafTimer !== null && window.cancelAnimationFrame(state.rafTimer);
};
const leave = () => {
state.isHover = false; //开启_move
move();
};
const over = () => {
_cancel();
};
项目地址:http://1.94.195.239:8000/#/home
完整代码
<template>
<div class="fs-estimated-virtuallist-container">
<div
class="fs-estimated-virtuallist-content"
ref="contentRef"
@mouseleave="leave"
@mouseenter="enter"
@mouseover="over"
@scroll="handleScroll"
>
<div class="fs-estimated-virtuallist-list" ref="listRef" :style="scrollStyle">
<div class="fs-estimated-virtuallist-list-item" ref="itemRef" v-for="i in renderList" :key="i.project_id">
<slot name="item" :item="i"></slot>
</div>
</div>
<div class="fs-estimated-virtuallist-list" :style="scrollStyle2">
<div class="fs-estimated-virtuallist-list-item" v-for="i in virList" :key="i.project_id">
<slot name="item" :item="i"></slot>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { type CSSProperties, computed, onMounted, reactive, ref, watch, PropType, nextTick } from 'vue';
import type { VirtualStateType } from './data';
import { delayRef } from '@/utils/base';
import { GitHubItem } from '@/pages/home/composables/use-github';
import { debounce } from 'lodash';
import { slice } from 'lodash-es';
const props = defineProps({
loading: {
type: Boolean,
required: true
},
dataSource: {
type: Array as PropType<GitHubItem[]>,
required: true
}
});
const emit = defineEmits(['scroll-end']);
const propList = ref<GitHubItem[]>([]);
const contentRef = ref<HTMLDivElement>();
const listRef = ref<HTMLDivElement>();
const itemRef = ref<HTMLDivElement[]>();
// const positions = ref<IPosInfo[]>([]);
/**
* @param viewHeight 当前可视区域高度
* @param itemHeight 每个item的高度
* @param startIndex 当前可视区域的起始索引
* @param maxCount 当前可视区域的最大索引
* @params isHove
* **/
const state = reactive<VirtualStateType>({
viewHeight: 0,
itemHeight: 0,
startIndex: 0,
maxCount: 1,
preLen: 0,
rafTimer: null,
isHover: false
});
const endIndex = computed(() => Math.min(propList.value.length, state.startIndex + state.maxCount));
const renderList = computed(() => propList.value.slice(state.startIndex, endIndex.value));
const virList = computed(() => propList.value.slice(0, state.maxCount));
// 复制多几个用来无缝滚动的
const scrollStyle = computed(
() =>
({
height: `${state.itemHeight * renderList.value.length}px`,
transform: `translate3d(0, ${state.itemHeight * state.startIndex}px, 0)`
} as CSSProperties)
);
const scrollStyle2 = computed(
() =>
({
transform: `translate3d(0, ${state.itemHeight * state.startIndex}px, 0)`
} as CSSProperties)
);
/**
* @description 播放滚动 虚拟滚动
*/
const move = () => {
if (state.isHover) return;
// 加载中就停止
if (props.loading) {
_cancel();
return;
}
contentRef.value!.scrollTop += 1;
state.rafTimer = requestAnimationFrame(move);
// setTimeout(() => {
// move();
// }, 50);`
};
const _cancel = () => {
console.log(state.rafTimer, 'rafTimer');
state.rafTimer !== null && window.cancelAnimationFrame(state.rafTimer);
};
const enter = () => {
state.isHover = true; //关闭_move
_cancel();
};
const leave = () => {
state.isHover = false; //开启_move
move();
};
const over = () => {
_cancel();
};
// 节流
function rafThrottle(fn: Function) {
let lock = false;
return function (this: any, ...args: any[]) {
if (lock) return;
lock = true;
window.requestAnimationFrame(() => {
fn.apply(this, args);
lock = false;
});
};
}
const handleScroll = rafThrottle(() => {
let { scrollTop } = contentRef.value!;
// 计算启示索引 容器滚动的高度/单个数据的高度
// 获取到当前滚动到的索引
state.startIndex = Math.floor(scrollTop / state.itemHeight);
console.log(state.startIndex, 'startIndex');
if (renderList.value.length === 0) {
contentRef.value!.scrollTop = 0;
!props.loading && emit('scroll-end');
}
});
/**
* @description 初始化
*/
const init = () => {
// 初始化容器高度
state.viewHeight = contentRef.value ? contentRef.value.offsetHeight : 0;
};
/**
* @description 将新拿到的列表赋值给propList
*/
const addNewList = () => {
propList.value = props.dataSource;
console.log(propList.value, 'value');
};
/**
* @description 重新计算最大数
*/
const handleSetPosition = () => {
state.viewHeight = contentRef.value ? contentRef.value.offsetHeight : 0;
nextTick(() => {
if (itemRef.value) {
// 拿数组第一项计算最大 也就是计算item的高度可以放下多少个item
state.maxCount = Math.ceil(state.viewHeight / itemRef.value[0].offsetHeight) + 1;
state.itemHeight = itemRef.value[0].offsetHeight;
}
});
};
/**
* @description 监听容器宽高变化
* */
const observeContent = () => {
if (contentRef.value) {
const observer = new ResizeObserver(debounce(handleSetPosition, 300));
observer.observe(contentRef.value);
}
};
onMounted(() => {
//初始化做两件事情
// 监听容器宽高变化
// 1.计算最大数 maxCount
// 2.计算item的高度 itemHeight
// 滚动时 计算当前滚动到的索引 state.startIndex
// 将新拿到的列表赋值给propList
observeContent();
init();
});
watch(
() => props.dataSource.length,
() => {
// 接受到的列表高度变化
addNewList();
handleSetPosition();
}
);
watch(
() => props.loading,
value => {
// 加载完之后开始动画
if (!value) {
move();
}
}
);
</script>
<style scoped lang="scss">
// 隐藏滚动条
::-webkit-scrollbar {
display: none;
background-color: transparent;
}
.fs-estimated-virtuallist {
&-container {
position: relative;
width: 100%;
height: 100%;
}
&-content {
position: absolute;
width: 100%;
height: 100%;
overflow: auto;
}
&-list-item {
box-sizing: border-box;
width: 100%;
}
}
</style>
欢迎大家积极讨论,可以加我微信,后期建群共同学习前端,也会发一下接单信息方便大家做一些兼职。
作者微信:xiaoyukejikun,后期建群共同学习与讨论。