小程序实现触底加载跟下拉刷新
下拉刷新和触底加载的使用场景以及业务需求
- 下拉刷新
- 主要应用于项目的首页或者需要实时数据的场景
- 业务需求
- 提供实时数据(避免数据更新后仍显示旧数据的问题)
- 网络错误的恢复
- 给用户一个主动刷新的方式
- 避免强制刷新导致的用户体验感不好
- 触底加载
- 主要应用于项目当中的长列表(比如猜你喜欢,多tabs推荐列表,订单列表)
- 业务需求
- 减少首屏加载时间(优化性能,避免一次加载过多导致卡顿)
- 提高用户的体验感(不需要用户去手动的触发加载更多数据)
实现下拉刷新和触底加载的思路
下拉刷新实现
(1)监听用户下拉动作
通过小程序提供的onPullDownRefresh
生命周期或scroll-view
的refresherrefresh
属性触发刷新逻辑。
- 如何区分用户主动下拉还是页面的初始化
- 使用onLoad生命周期存储页面初始化的标记信息,比如设置一个boolean值为true,用户触发下拉刷新时重置数据并改变初始化信息为false
(2)触发数据加载
调用API获取最新数据,清空旧数据列表,然后在追加数据。
- Api请求失败或超时的处理方法
- 使用toast进行错误提示并通过
wx.stopPullDownRefresh停止动画
- 使用toast进行错误提示并通过
(3)停止刷新动画
加载完成后使用wx.stopPullDownRefresh
停止动画。
- 数据加载慢导致动画卡顿的处理方法
- 设置定时器强制停止动画(例如5秒),避免长时间卡顿。
触底加载实现
(1)监听滚动事件
通过scroll-view
的scrolltolower
事件或onReachBottom
生命周期监听滚动触底
- 如何避免频繁触发触底事件
- 添加防抖或节流逻辑(如300毫秒延迟),避免频繁触发请求。
(2)触发数据加载
分页加载需维护当前页码,每次触底递增页码并请求对应数据。
- 如何判断没有更多数据
- 根据API返回的
total
字段或空数组用于判断有无更多数据,设置标志停止后续请求。
- 根据API返回的
(3)将新数据追加到列表
- 数据错位或重复
- 根据返回的数据按照时间倒序排列,前端按照顺序添加
关键问题与优化方案
数据同步与性能
下拉刷新时,若数据量过大可能导致渲染延迟。建议优先加载首屏关键数据,非关键数据异步处理。触底加载的分页大小需根据接口性能动态调整(如初始10条,后续20条)。
错误处理与用户体验
网络请求失败时,除Toast提示外,可提供手动重试按钮。触底加载无更多数据时,显示友好提示(如“已加载全部内容”)。列表渲染使用key
属性确保数据更新效率,避免重复渲染。
代码结构示例
下拉刷新
<script setup lang="ts">
//下拉刷新状态
const isTriggered =ref(false)
// 自定义下拉刷新被触发
const onRefresherrefresh=async()=>{
// 开启动画
isTriggered.value = true
//重置猜你喜欢组件数据
guessRef.value?.resetData()//加载数据
await Promise.all([其他数据加载完成])
isTriggered.value = false
</script>
<scrol1-view
refresher-enabled
@refresherrefresh="onRefresherrefresh":refresher-triggered="isTriggered"
class="scrol1-view"
scro11-y>
..省略
</scrol1-view>
触底加载(分页加载)
const goodsList = ref([])
//已结束标记
const finish = ref(false)
//获取数据
const getGoodsData=async()=>{
//退出分页判断
if(finish.value === true){
return uni.showToast({ icon:'none',title:'没有更多数据~'})
const res = await getGoodsAPI(pageParams)
// 数组追加
guessList.value.push(...res.result.items)
// 分页条件
if(pageParams.page<res.result.pages){
// 页码累加
pageParams.page++
}else {
finish.value = true
// 重置数据
const resetData=()=>{
pageParams.page =1guessList.value=[]
finish.value = false
const isTriggered =ref(false)
// 自定义下拉刷新被触发
const onRefresherrefresh=async()=>{
// 开启动画
isTriggered.value = true
// 重置数据
goodsRef.value?.resetData()
await Promise.all([获取数据的逻辑])
isTriggered.value = false
}
注意事项
- 动画流畅性:下拉刷新动画时长应匹配数据加载时间,可设置最小显示时长(如1秒)避免闪烁。
- 数据一致性:分页加载时,确保后端数据排序稳定(如按创建时间降序),前端严格按顺序追加。
- 内存管理:超长列表需结合虚拟滚动技术(如
recycle-view
),防止DOM节点过多导致性能下降。
原生Js实现下拉刷新和触底加载
下拉刷新
- 监听触摸事件
- touchstart:记录触摸开始的位置。
- touchmove:计算触摸移动的距离,判断是否达到下拉刷新的阈值。
- touchend:在触摸结束时,如果达到阈值,则触发刷新操作。
- 添加刷新指示器: 在页面顶部添加一个刷新指示器(如旋转的图标或进度条),在触发下拉刷新时显示。
- 执行刷新操作:在达到下拉刷新阈值后,执行实际的刷新操作(如重新加载数据)。 刷新完成后,隐藏刷新指示器。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>下拉刷新示例</title>
<style>
body, html {
margin: 0;
padding: 0;
height: 100%;
overflow: hidden;
}
.refresh-indicator {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 50px;
background-color: #f0f0f0;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.3s;
}
.refresh-indicator.active {
opacity: 1;
}
.content {
height: 100%;
overflow-y: scroll;
-webkit-overflow-scrolling: touch;
}
</style>
</head>
<body>
<div class="refresh-indicator">下拉刷新...</div>
<div class="content">
<!-- 页面内容 -->
<h1>下拉刷新示例</h1>
<p>这里是页面内容...</p>
</div>
<script>
const refreshIndicator = document.querySelector('.refresh-indicator');
const content = document.querySelector('.content');
let startY = 0;
let distance = 0;
const threshold = 50; // 下拉刷新的阈值
content.addEventListener('touchstart', (e) => {
startY = e.touches[0].clientY;
});
content.addEventListener('touchmove', (e) => {
const currentY = e.touches[0].clientY;
distance = currentY - startY;
if (distance > 0 && distance <= threshold) {
refreshIndicator.style.transform = `translateY(${distance}px)`;
refreshIndicator.style.opacity = distance / threshold;
}
});
content.addEventListener('touchend', () => {
if (distance > threshold) {
// 触发刷新操作
refreshIndicator.classList.add('active');
refreshData().then(() => {
// 刷新完成后隐藏刷新指示器
refreshIndicator.classList.remove('active');
// 重置位置
refreshIndicator.style.transform = '';
refreshIndicator.style.opacity = '';
distance = 0;
});
} else {
// 未达到阈值,重置位置
refreshIndicator.style.transform = '';
refreshIndicator.style.opacity = '';
distance = 0;
}
});
function refreshData() {
return new Promise((resolve) => {
// 模拟刷新操作
setTimeout(() => {
console.log('数据已刷新');
resolve();
}, 2000);
});
}
</script>
</body>
</html>
触底加载(分页加载)
- 监听滚动事件: 监听 scroll 事件,计算当前滚动位置是否接近页面底部。
- 触发加载操作: 当滚动位置接近页面底部时,触发加载更多数据的操作。
- 显示加载状态: 在加载过程中,显示加载指示器(如旋转的图标或进度条)。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>触底加载示例</title> <style> body, html { margin: 0; padding: 0; height: 100%; overflow: hidden; } .content { height: 100%; overflow-y: scroll; -webkit-overflow-scrolling: touch; } .item { height: 100px; line-height: 100px; text-align: center; border-bottom: 1px solid #ccc; } .loading-indicator { text-align: center; padding: 20px; display: none; } .loading-indicator.active { display: block; } </style> </head> <body> <div class="content"> <!-- 页面内容 --> <div class="item">Item 1</div> <div class="item">Item 2</div> <div class="item">Item 3</div> <!-- 更多项 --> <div class="loading-indicator">加载中...</div> </div> <script> const content = document.querySelector('.content'); const loadingIndicator = document.querySelector('.loading-indicator'); let currentPage = 1; const pageSize = 10; function loadMoreItems() { return new Promise((resolve) => { // 模拟加载更多数据 setTimeout(() => { for (let i = 0; i < pageSize; i++) { const item = document.createElement('div'); item.className = 'item'; item.textContent = `Item ${currentPage * pageSize + i + 1}`; content.appendChild(item); } currentPage++; resolve(); }, 2000); }); } content.addEventListener('scroll', () => { const scrollTop = content.scrollTop; const scrollHeight = content.scrollHeight; const clientHeight = content.clientHeight; if (scrollTop + clientHeight >= scrollHeight - 5) { // 触发加载更多数据 loadingIndicator.classList.add('active'); loadMoreItems().then(() => { loadingIndicator.classList.remove('active'); }); } }); </script> </body> </html>
长列表渲染优化的另一种方式
虚拟列表
虚拟列表(Virtual List)是为了优化长列表的性能,通过仅渲染可视区域内的元素,减少DOM节点数量,从而提高渲染效率和用户体验,也可以用于实现无限滚动
vue中实现虚拟列表
使用第三方库vue-virtual-scroll-list
<template>
<virtual-list
:size="50" <!-- 每个列表项的高度 -->
:remain="10" <!-- 可视区域内保留的项数 -->
:data-key="'id'"
:data-sources="items"
:data-component="ListItem"
/>
</template>
<script>
import VirtualList from 'vue-virtual-scroll-list';
import ListItem from './ListItem.vue'; // 自定义的列表项组件
export default {
components: {
VirtualList,
ListItem
},
data() {
return {
items: Array.from({ length: 1000 }, (_, index) => ({ id: index, content: `Item ${index}` }))
};
}
};
</script>
<template>
<div class="list-item">
{{ item.content }}
</div>
</template>
<script>
export default {
props: {
item: Object
}
};
</script>
<style>
.list-item {
height: 50px;
line-height: 50px;
border-bottom: 1px solid #eee;
}
</style>
react中实现虚拟列表
React中实现虚拟列表同样可以使用第三方库如react-window或react-virtualized
import React from 'react';
import { FixedSizeList as List } from 'react-window';
const Row = ({ index, style }) => (
<div className={`list-item ${index % 2 ? 'odd' : 'even'}`} style={style}>
Item {index}
</div>
);
const VirtualList = () => {
const itemCount = 1000;
const itemSize = 50;
return (
<List
height={800}
itemCount={itemCount}
itemSize={itemSize}
width={300}
>
{Row}
</List>
);
};
export default VirtualList;
第三方组件库
有些第三方组件库已经实现了下拉刷新或虚拟列表,触底加载等逻辑,可以直接使用,例如vant,antd-mobile等