Vant List 组件中双向绑定 loading 的注意事项
问题现象
在使用 Vant 的 van-list 组件时,经常会遇到 loading 状态一直为 true,导致页面一直显示加载中状态,无法正常进行分页加载的问题。
原因分析
van-list 的工作机制
van-list 的 v-model:loading 是一个双向绑定属性,它的工作流程如下:
当用户滚动到列表底部时,
van-list会自动设置loading = true同时触发
@load事件开发者在
@load事件中处理数据加载数据加载完成后,开发者需要手动设置
loading = false
常见错误写法
错误 1:在数据加载函数中检查 loading 状态
// ❌ 错误写法:会导致请求被阻止
const fetchNoticeData = async (pageNum: number = 1) => {
if (loading.value) return; // ERROR: 这里会阻止请求执行
try {
loading.value = true;
const response = await getNotices({
pageNum: pageNum,
pageSize: page.value.pageSize,
});
// ... 处理数据
} finally {
loading.value = false;
}
};问题分析: 当 van-list 触发 @load 事件时,已经自动设置了 loading = true,此时进入 fetchNoticeData 函数会被 if (loading.value) return 直接阻止执行,导致:
请求永远不会发送
loading状态永远不会被重置为false页面一直显示加载中状态
错误 2:在 load 事件处理中检查 loading 状态
// ❌ 错误写法:多余的 loading 检查
const handleLoadMore = () => {
if (!finished.value && !loading.value) {
// ERROR: 这里会阻止请求执行
fetchNoticeData(page.value.current + 1);
}
};问题分析: van-list 触发 @load 事件时 loading 已经是 true,这个检查会阻止函数执行。
正确的使用方法
方法一:配合 van-list 的状态管理
// ✅ 正确写法
const fetchData = async (pageNum = 1) => {
try {
// 不要手动设置 loading = true,让 van-list 管理
const response = await api.getData({
pageNum,
pageSize: 10,
});
// 处理数据
if (pageNum === 1) {
list.value = response.data.records;
} else {
list.value.push(...response.data.records);
}
// 判断是否加载完成
finished.value = list.value.length >= response.data.total;
} catch (error) {
console.error("数据加载失败:", error);
} finally {
loading.value = false; // 只在完成时设置为 false
}
};
const handleLoadMore = () => {
if (!finished.value) {
fetchData(page.current + 1);
}
};方法二:完全手动管理
// ✅ 如果需要完全手动控制
const fetchData = async (pageNum = 1) => {
try {
loading.value = true;
const response = await api.getData({
pageNum,
pageSize: 10,
});
// 处理数据...
} catch (error) {
console.error("数据加载失败:", error);
} finally {
loading.value = false;
}
};
const handleLoadMore = () => {
// 总是调用,让 fetchData 内部判断
fetchData(page.current + 1);
};基于实际项目的正确示例
<script lang="ts" setup>
import { formatDate, getAssetImage } from "@/utils";
import {
NoticeItem,
NoticeType,
} from "@/hooks/views/home/default/components/useNotice";
import { getNotices } from "@/service/api/default";
const router = useRouter();
const contentList = ref<NoticeItem[]>([]);
const loading = ref(false);
const finished = ref(false);
const page = ref({
current: 1,
pageSize: 10,
total: 0,
});
const drawer = defineModel<boolean>("drawer");
const fetchNoticeData = async (pageNum: number = 1) => {
// if (loading.value) return //ERROR: 这里会阻止请求执行
try {
loading.value = true;
const response = await getNotices({
pageNum: pageNum,
pageSize: page.value.pageSize,
});
const formattedRecords = response.data.records.map((item: NoticeItem) => ({
...item,
releaseTime: formatDate(item.releaseTime.toString(), "YYYY-MM-DD"),
ymdhms: formatDate(item.releaseTime.toString(), "YYYY-MM-DD HH:mm:ss"),
}));
if (pageNum === 1) {
contentList.value = formattedRecords;
} else {
contentList.value.push(...formattedRecords);
}
page.value.current = response.data.current;
page.value.total = response.data.total;
finished.value = contentList.value.length >= response.data.total;
} catch (error) {
console.error("获取公告数据失败:", error);
} finally {
loading.value = false;
}
};
const handleLoadMore = () => {
// if (!finished.value && !loading.value) { //ERROR: 这里会阻止请求执行
if (!finished.value) {
fetchNoticeData(page.value.current + 1);
}
};
const resetDataState = () => {
contentList.value = [];
page.value.current = 1;
finished.value = false;
};
const handleDrawerOpen = () => {
resetDataState();
fetchNoticeData(1);
};
const handleDrawerClose = () => {
drawer.value = false;
};
const handleNavigateToDetail = (item: NoticeItem) => {
try {
router.push({
path: "/noticeDetail",
state: { data: JSON.parse(JSON.stringify(item)) },
});
} catch (error) {
console.error("跳转详情页失败:", error);
}
};
const handleNoticeItemClick = (item: NoticeItem) => {
if (loading.value) return;
handleNavigateToDetail(item);
};
</script>
<template>
<div class="drawer-container">
<van-popup
v-model:show="drawer"
position="bottom"
class="bottom-drawer"
safe-area-inset-bottom
:closeable="true"
@close="handleDrawerClose"
@open="handleDrawerOpen"
:overlay-style="{ backgroundColor: 'rgba(0, 0, 0, 0.1)' }"
>
<div class="drawer-header">
<h4>公告</h4>
</div>
<div class="drawer-content">
<van-list
v-model:loading="loading"
:finished="finished"
:finished-text="contentList.length > 0 ? '没有更多了' : ''"
:immediate-check="false"
@load="handleLoadMore"
>
<template v-if="contentList.length > 0">
<div
class="notice-item"
v-for="item in contentList"
:key="item.id"
@click="handleNoticeItemClick(item)"
>
<div class="notice-item-header">
<div class="title-box">
<span class="label">{{ NoticeType[item.type] }}</span>
<span class="title">{{ item.title }}</span>
</div>
<div class="time-box">
<span class="icon">
<img
:src="getAssetImage('icons/notice_time.png')"
alt="时间图标"
/>
</span>
<span class="time">{{ item.releaseTime }}</span>
</div>
</div>
<div class="notice-item-content">
<span
class="notice-item-content-text"
v-html="item.content"
></span>
</div>
</div>
</template>
<template v-else>
<div class="empty">
<div class="empty-box">
<img
:src="getAssetImage('images/empty_data.png')"
alt="暂无数据"
/>
<span> 暂无数据,请先添加公告</span>
</div>
</div>
</template>
</van-list>
</div>
</van-popup>
</div>
</template>
<style scoped lang="scss">
:deep(.bottom-drawer) {
border-radius: 0.703rem 0.703rem 0px 0px;
padding: 0.844rem 0.703rem 0.703rem 0.703rem;
min-height: 65vh;
box-sizing: border-box;
overflow: auto;
.van-popup__close-icon {
color: #121432;
}
.drawer-header {
padding: 0;
color: var(--el-color-primary);
margin-bottom: 0.469rem;
font-size: 0.656rem;
}
.drawer-content {
padding: 0;
.notice-item {
padding: 0.563rem 0;
border-bottom: 1px solid #f0f1f6;
cursor: pointer;
.notice-item-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0.469rem;
.title-box {
flex: 1;
display: flex;
align-items: center;
// 超出省略
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
.label {
font-size: 0.469rem;
color: white;
padding: 0.188rem 0.469rem;
line-height: 0.563rem;
background-color: var(--el-color-primary);
border-radius: 0.094rem;
margin-right: 0.469rem;
}
.title {
font-size: 0.656rem;
font-weight: 600;
color: var(--el-color-primary);
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 100%;
}
}
.time-box {
display: flex;
align-items: stretch;
font-size: 0.469rem;
.icon {
font-weight: 600;
img {
width: 0.563rem;
height: 0.563rem;
}
margin-right: 0.234rem;
}
}
}
.notice-item-content {
font-size: 0.563rem;
font-weight: 400;
color: #5c5e62;
width: 100%;
}
.notice-item-content-text {
display: -webkit-box;
line-clamp: 2;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
line-height: 0.844rem;
word-break: break-all;
}
}
.empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 45vh;
.empty-box {
display: flex;
flex-direction: column;
align-items: center;
img {
width: 4.313rem;
height: 2.907rem;
}
span {
margin-top: 0.469rem;
font-size: 0.563rem;
color: #98999c;
}
}
}
.van-list__finished-text {
font-size: 0.469rem;
color: #98999c;
}
}
}
</style>总结
van-list 的双向绑定 loading 机制虽然方便,但需要理解其工作原理。关键点是:
van-list会自动管理loading状态的设置开发者只需要在数据加载完成后重置
loading = false避免在加载函数中进行
loading状态检查确保每次
@load事件都能正确结束并重置状态
正确理解和使用这个机制,可以避免很多常见的加载状态问题。