原生JavaScript实现无限滚动加载效果
一、功能背景与应用场景
无限滚动加载(Infinite Scroll)是现代 Web 应用最主流的列表加载方式,替代传统分页,大幅提升用户浏览体验,常见场景:
- 商品列表页、推荐流
- 评论区、消息列表
- 文章列表、动态广场
- 后台数据表格、日志列表
基础版本容易出现:重复请求、加载闪烁、滚动抖动、结束状态不显示、性能差等问题。本文实现企业级稳定版,解决所有常见坑点。
二、核心实现效果
滚动到底部自动加载数据
防重复请求(加锁机制)
加载中状态提示(Loading)
无数据 / 全部加载完毕状态提示
滚动监听节流优化
列表渲染无闪烁、无抖动
支持异步接口模拟(可直接对接后端)
响应式适配,移动端 / PC 端通用
代码模块化、可配置、易扩展
三、完整可运行源码(直接复制发布)
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>原生JS实现无限滚动加载(下拉加载更多)</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="external nofollow" >
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: "Microsoft YaHei", sans-serif;
}
body {
background-color: #f5f7fa;
padding: 20px;
}
.container {
max-width: 800px;
margin: 0 auto;
}
.title {
font-size: 24px;
color: #2c3e50;
margin-bottom: 20px;
text-align: center;
}
/* 列表容器 */
.list-container {
display: flex;
flex-direction: column;
gap: 15px;
margin-bottom: 30px;
min-height: 300px;
}
/* 列表项 */
.list-item {
background: #fff;
padding: 20px;
border-radius: 10px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
transition: transform 0.2s;
}
.list-item:hover {
transform: translateY(-2px);
}
.list-item h3 {
font-size: 18px;
color: #2f3542;
margin-bottom: 8px;
}
.list-item p {
font-size: 14px;
color: #57606f;
line-height: 1.6;
}
/* 加载状态 */
.load-status {
text-align: center;
padding: 15px 0;
font-size: 14px;
color: #666;
display: none;
}
.load-status.active {
display: block;
}
.loading-icon {
display: inline-block;
animation: rotate 1s linear infinite;
margin-right: 6px;
}
@keyframes rotate {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 无更多数据 */
.no-more {
color: #999;
padding: 10px 0;
}
</style>
</head>
<body>
<div class="container">
<h1 class="title">无限滚动加载演示</h1>
<div class="list-container" id="listContainer"></div>
<div class="load-status" id="loadStatus">
<i class="fas fa-spinner loading-icon"></i>
<span>正在加载更多数据...</span>
</div>
<div class="load-status no-more" id="noMore">已加载全部数据</div>
</div>
<script>
// ====================== 配置项 ======================
const CONFIG = {
pageSize: 8, // 每页条数
threshold: 150, // 距离底部多少像素触发加载
throttleDelay: 150, // 滚动节流时间
totalData: 40 // 模拟总数据量
};
// ====================== 状态管理 ======================
let currentPage = 1;
let isLoading = false; // 防重复请求锁
let isEnd = false; // 是否全部加载完毕
// DOM
const listContainer = document.getElementById('listContainer');
const loadStatus = document.getElementById('loadStatus');
const noMore = document.getElementById('noMore');
// ====================== 初始化 ======================
function init() {
loadData();
window.addEventListener('scroll', throttle(handleScroll, CONFIG.throttleDelay));
}
// ====================== 滚动监听(触底判断) ======================
function handleScroll() {
if (isLoading || isEnd) return;
const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
const clientHeight = document.documentElement.clientHeight;
const scrollHeight = document.documentElement.scrollHeight;
// 触底判断
if (scrollTop + clientHeight + CONFIG.threshold >= scrollHeight) {
currentPage++;
loadData();
}
}
// ====================== 加载数据(模拟接口) ======================
function loadData() {
isLoading = true;
loadStatus.classList.add('active');
// 模拟请求延迟
setTimeout(() => {
const start = (currentPage - 1) * CONFIG.pageSize;
const end = currentPage * CONFIG.pageSize;
// 构造数据
const list = [];
for (let i = start; i < end && i < CONFIG.totalData; i++) {
list.push({
id: i + 1,
title: `这是第 ${i + 1} 条内容`,
content: '原生JS实现无限滚动加载,支持防重复请求、滚动节流、状态管理,企业级稳定方案。'
});
}
// 渲染
renderList(list);
// 判断是否加载完毕
if (end >= CONFIG.totalData) {
isEnd = true;
loadStatus.classList.remove('active');
noMore.classList.add('active');
} else {
isLoading = false;
loadStatus.classList.remove('active');
}
}, 800);
}
// ====================== 渲染列表 ======================
function renderList(data) {
data.forEach(item => {
const div = document.createElement('div');
div.className = 'list-item';
div.innerHTML = `
<h3>${item.title}</h3>
<p>${item.content}</p>
`;
listContainer.appendChild(div);
});
}
// ====================== 节流函数 ======================
function throttle(fn, delay) {
let lastTime = 0;
return function (...args) {
const now = Date.now();
if (now - lastTime >= delay) {
lastTime = now;
fn.apply(this, args);
}
};
}
// 启动
init();
</script>
</body>
</html>四、核心技术讲解
1. 触底判断公式(最关键)
scrollTop + clientHeight + threshold >= scrollHeight
scrollTop:滚动条滚动距离clientHeight:可视窗口高度threshold:距离底部提前触发距离scrollHeight:页面总高度
只要满足公式,就判定即将触底,开始加载下一页。
2. 防重复请求(企业必备)
使用isLoading 锁:
- 开始请求 →
isLoading = true - 结束请求 →
isLoading = false - 滚动时判断锁状态,避免多次触发
3. 性能优化:滚动节流
滚动事件触发频率极高,必须使用节流:
- 每 150ms 只执行一次
- 大幅降低浏览器性能消耗
- 避免页面卡顿
4. 状态管理规范
- 加载中 → 显示 Loading
- 无数据 → 显示 “已加载全部”
- 结束后不再监听滚动
- 全程无闪烁、无抖动
5. 扩展性极强
可直接对接真实接口:
- 替换
setTimeout为fetch/axios - 后端返回总条数即可判断是否结束
五、可扩展高级功能(文章加分项)
- 错误重试:加载失败显示重试按钮
- 下拉刷新:配合 touch 事件实现下拉刷新
- 缓存机制:使用 localStorage 缓存列表
- 虚拟列表:支持 10w+ 数据不卡顿(超高分亮点)
- 淡入动画:加载新项目时添加动画
- 定位滚动:返回后保持上次位置
六、适用场景
- 商品列表
- 评论列表
- 消息流
- 后台管理表格
- 文章 / 动态列表
所有长列表业务都能直接使用。
七、方法补充
原生 JavaScript 实现无限滚动加载是一种常见的列表分页技术,核心是监听滚动事件,当用户滚动到页面底部(或接近底部)时,自动加载下一页数据并追加到列表中。以下是一个完整的实现方案,包含核心原理、代码示例及优化技巧。
核心原理
- 监听滚动事件:使用
window或特定容器的scroll事件。 - 计算滚动位置:判断
scrollTop + clientHeight >= scrollHeight - threshold(threshold 为阈值,如 200px)。 - 触发加载:满足条件时,调用加载函数,请求新数据。
- 追加内容:将新数据渲染到页面列表末尾。
- 状态控制:使用
isLoading标志防止重复请求,使用hasMore标志表示是否还有更多数据。
实现代码
HTML 结构
<div id="app"> <ul id="list" class="list"></ul> <div id="loading" class="loading hidden">加载中...</div> <div id="no-more" class="no-more hidden">没有更多数据了</div> </div>
CSS 样式(简单示例)
.list {
list-style: none;
padding: 0;
margin: 0;
}
.list li {
padding: 12px;
border-bottom: 1px solid #eee;
}
.loading, .no-more {
text-align: center;
padding: 10px;
color: #999;
}
.hidden {
display: none;
}JavaScript 实现
// 模拟异步数据请求
function fetchData(page, pageSize = 10) {
return new Promise((resolve) => {
setTimeout(() => {
const start = (page - 1) * pageSize;
const items = Array.from({ length: pageSize }, (_, i) => ({
id: start + i + 1,
content: `条目 ${start + i + 1}`
}));
// 模拟总共 50 条数据
const hasMore = start + pageSize < 50;
resolve({ items, hasMore });
}, 500);
});
}
class InfiniteScroll {
constructor(options) {
this.container = options.container; // 滚动容器,默认为 window
this.listEl = options.listEl; // 列表容器元素
this.loadingEl = options.loadingEl; // 加载提示元素
this.noMoreEl = options.noMoreEl; // 无更多数据提示元素
this.fetchData = options.fetchData; // 获取数据的函数
this.page = 1;
this.pageSize = options.pageSize || 10;
this.isLoading = false;
this.hasMore = true;
this.threshold = options.threshold || 200; // 距离底部多少像素时触发
this.init();
}
init() {
this.loadMore = this.loadMore.bind(this);
// 绑定滚动事件(使用节流优化)
this.scrollHandler = this.throttle(this.checkScroll.bind(this), 200);
this.container.addEventListener('scroll', this.scrollHandler);
if (this.container === window) {
window.addEventListener('scroll', this.scrollHandler);
}
// 初始加载第一页
this.loadMore();
}
// 检查是否需要加载
checkScroll() {
if (!this.hasMore || this.isLoading) return;
let scrollTop, clientHeight, scrollHeight;
if (this.container === window) {
scrollTop = window.pageYOffset || document.documentElement.scrollTop;
clientHeight = document.documentElement.clientHeight;
scrollHeight = document.documentElement.scrollHeight;
} else {
scrollTop = this.container.scrollTop;
clientHeight = this.container.clientHeight;
scrollHeight = this.container.scrollHeight;
}
if (scrollTop + clientHeight >= scrollHeight - this.threshold) {
this.loadMore();
}
}
// 加载更多数据
async loadMore() {
if (!this.hasMore || this.isLoading) return;
this.isLoading = true;
this.showLoading(true);
try {
const { items, hasMore } = await this.fetchData(this.page, this.pageSize);
this.renderItems(items);
this.hasMore = hasMore;
this.page++;
if (!this.hasMore) {
this.showNoMore(true);
}
} catch (error) {
console.error('加载失败', error);
// 可在此处显示错误提示,并提供重试按钮
} finally {
this.isLoading = false;
this.showLoading(false);
}
}
// 渲染列表项
renderItems(items) {
const fragment = document.createDocumentFragment();
items.forEach(item => {
const li = document.createElement('li');
li.textContent = item.content;
fragment.appendChild(li);
});
this.listEl.appendChild(fragment);
}
// 显示/隐藏加载提示
showLoading(show) {
if (this.loadingEl) {
this.loadingEl.classList.toggle('hidden', !show);
}
}
// 显示/隐藏无更多数据提示
showNoMore(show) {
if (this.noMoreEl) {
this.noMoreEl.classList.toggle('hidden', !show);
}
}
// 节流函数
throttle(fn, delay) {
let last = 0;
return function(...args) {
const now = Date.now();
if (now - last > delay) {
last = now;
fn.apply(this, args);
}
};
}
// 销毁,移除事件监听
destroy() {
if (this.container === window) {
window.removeEventListener('scroll', this.scrollHandler);
} else {
this.container.removeEventListener('scroll', this.scrollHandler);
}
}
}
// 使用示例
const infiniteScroll = new InfiniteScroll({
container: window, // 滚动容器
listEl: document.getElementById('list'),
loadingEl: document.getElementById('loading'),
noMoreEl: document.getElementById('no-more'),
fetchData: fetchData, // 自定义数据获取函数
pageSize: 10,
threshold: 200
});八、总结
本文实现的无限滚动加载组件是前端高频面试题 + 企业必备功能。特点:原生 JS、无依赖、防重复请求、性能高、状态完整、无闪烁。
以上就是原生JavaScript实现无限滚动加载效果的详细内容,更多关于JavaScript无限滚动加载的资料请关注脚本之家其它相关文章!
相关文章
一种Javascript解释ajax返回的json的好方法(推荐)
下面小编就为大家带来一篇一种Javascript解释ajax返回的json的好方法(推荐)。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧2016-06-06
javascript数组元素删除方法delete和splice解析
这篇文章主要介绍了javascaipt数组元素删除方法delete和splice解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下2019-12-12


最新评论