-
监听滚动事件 + 计算滚动位置(传统方案)
通过监听容器的滚动事件,计算是否已滚动到底部附近。
计算公式:滚动容器.scrollTop + 滚动容器.clientHeight >= 滚动容器.scrollHeight - 阈值
scrollTop:已滚动的距离
clientHeight:容器可视区域的高度
scrollHeight:整个内容的总高度
阈值:提前触发的距离
const container = document.getElementById('scroll-container'); // 或 window
let isLoading = false;
let currentPage = 1;
// 使用节流函数优化性能
container.addEventListener('scroll', throttle(() => {
// 计算是否滚动到底部(距离底部50px内触发)
const scrollBottom = container.scrollTop + container.clientHeight;
const totalHeight = container.scrollHeight;
const threshold = 50;
if (totalHeight - scrollBottom <= threshold && !isLoading && hasMoreData) {
loadMoreData();
}
}, 200)); // 200ms节流
async function loadMoreData() {
isLoading = true;
// 显示加载提示...
try {
const newData = await fetchData(++currentPage);
// 将新数据渲染到列表末尾
renderList(newData);
// 判断是否还有更多数据
if (newData.length < pageSize) hasMoreData = false;
} catch (error) {
// 错误处理
currentPage--;
} finally {
isLoading = false;
}
}
-
使用 Intersection Observer API(推荐)
现代浏览器提供的原生 API,用于异步监听目标元素与视窗的交叉状态。
React:
import React, { useEffect, useRef, useState } from 'react';
function InfiniteList() {
const [list, setList] = useState([]);
const [page, setPage] = useState(1);
const [loading, setLoading] = useState(false);
const [hasMore, setHasMore] = useState(true);
const observerTarget = useRef(null);
useEffect(() => {
const observer = new IntersectionObserver(
entries => {
if (entries[0].isIntersecting && !loading && hasMore) {
loadMore();
}
},
{ threshold: 1.0 }
);
if (observerTarget.current) {
observer.observe(observerTarget.current);
}
return () => {
if (observerTarget.current) {
observer.unobserve(observerTarget.current);
}
};
}, [loading, hasMore]);
const loadMore = async () => {
setLoading(true);
const newData = await fetchData(page + 1);
setList(prev => [...prev, ...newData]);
setPage(prev => prev + 1);
if (newData.length === 0) setHasMore(false);
setLoading(false);
};
return (
<div>
{list.map(item => (
<div key={item.id}>{item.content}</div>
))}
{/* 哨兵元素 */}
<div ref={observerTarget}>
{loading && <div>加载中...</div>}
{!hasMore && <div>没有更多了</div>}
</div>
</div>
);
}
Vue:
<template>
<div class="container" ref="containerRef">
<!-- 列表内容 -->
<div v-for="item in list" :key="item.id" class="list-item">
{{ item.content }}
</div>
<!-- 底部加载状态 -->
<div ref="sentinelRef" class="sentinel">
<div v-if="loading" class="loading">加载中...</div>
<div v-if="!hasMore" class="no-more">没有更多了</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, nextTick } from 'vue'
// 响应式数据
const list = ref([])
const loading = ref(false)
const page = ref(1)
const hasMore = ref(true)
const containerRef = ref(null)
const sentinelRef = ref(null)
// 观察器实例
let observer = null
// 初始化观察器
const initObserver = () => {
// 清理旧的观察器
if (observer) {
observer.disconnect()
}
observer = new IntersectionObserver(
(entries) => {
const entry = entries[0]
if (entry.isIntersecting && !loading.value && hasMore.value) {
loadMore()
}
},
{
root: containerRef.value, // 可滚动的容器
rootMargin: '100px', // 提前100px触发
threshold: 0.1, // 10%可见时触发
}
)
if (sentinelRef.value) {
observer.observe(sentinelRef.value)
}
}
// 加载更多数据
const loadMore = async () => {
if (loading.value || !hasMore.value) return
loading.value = true
try {
// 模拟API请求
const newData = await fetchData(page.value)
if (newData.length > 0) {
list.value.push(...newData)
page.value++
// 重新观察哨兵元素(位置已变)
nextTick(() => {
if (observer && sentinelRef.value) {
observer.unobserve(sentinelRef.value)
observer.observe(sentinelRef.value)
}
})
} else {
hasMore.value = false
}
} catch (error) {
console.error('加载失败:', error)
} finally {
loading.value = false
}
}
// 模拟API请求
const fetchData = (pageNum) => {
return new Promise((resolve) => {
setTimeout(() => {
const pageSize = 10
const newItems = Array.from({ length: pageSize }, (_, i) => ({
id: (pageNum - 1) * pageSize + i + 1,
content: `项目 ${(pageNum - 1) * pageSize + i + 1}`,
}))
resolve(newItems)
}, 800)
})
}
// 生命周期
onMounted(() => {
// 首次加载
loadMore()
// 初始化观察器
initObserver()
})
onUnmounted(() => {
if (observer) {
observer.disconnect()
}
})
</script>
评论区