SSF0SSF0
首页
前端
  • Node
  • Go
  • Python
  • MySql
  • Bash
  • Git
  • Docker
  • VuePress
  • CI/CD
  • 服务器
  • 网站
  • 学习资料
  • 软件
  • 开源项目
Timeline
Github
标签
分类
首页
前端
  • Node
  • Go
  • Python
  • MySql
  • Bash
  • Git
  • Docker
  • VuePress
  • CI/CD
  • 服务器
  • 网站
  • 学习资料
  • 软件
  • 开源项目
Timeline
Github
标签
分类
  • HTML

    • html1
    • html2
  • CSS

    • Flex 布局常见问题与解决方案
  • JavaScript

    • 数据类型及引用问题
    • 处理 Blob 类型文件下载问题总结
    • localStorage 与 sessionStorage 区别
    • JavaScript 中的 script 标签问题详解
    • JavaScript 中的 this 指向问题详解
    • SessionStorage踩坑记录
    • 动态加载 JS 脚本方法对比
    • 浏览器页面关闭场景下的数据上报
    • HTTP流式接口详解与封装实践
    • JavaScript 中 this 绑定与解构:为什么 Pinia Store 可以解构,但 Class 实例方法不行?
  • es6

    • Promise
    • es6 模块导出方式全解析
    • reduce
    • forEach 中使用 async/await 的异步陷阱
  • Vue2

    • created VS mounted 发起异步请求
    • vue2-2
  • Vue3

    • Vite + Vue3 修改 element-plus 源码
    • Vue v-if 与 v-show
    • Vue3 ref 获取组件
    • Vue3 路由传参
    • 父子组件与组件里 Hooks 加载顺序
    • 第三方组件传入参数TS提示
    • Vue3 Props 在 Hooks 中的响应性处理
    • Vue Router 的两种历史模式及部署配置详解
    • 在Vue 3项目中顺利集成Tailwind CSS的完整指南
    • Vue Scoped CSS 深度解析
    • Vue 3 Teleport 组件完全指南:从原理到实战
    • Vant List 组件中双向绑定 loading 的注意事项
    • Vue 3 + Vite 中的路径别名与静态资源处理深度解析
    • 获取图片高度
    • 深入理解 Pinia:storeToRefs vs toRefs - 避开 TypeScript 类型推断的坑
  • webSocket

    • WebSocket 鉴权入门(实战版):握手票据 / 首帧鉴权 + 连接级 cid
  • Electron

    • 快速构建 electron + vue3 项目
  • TS

    • TS 泛型
    • 记录模板使用断言的问题
    • type 与 interface
  • WebPack

    • Webpack 介绍
  • Vite

    • Vite CLI 常见命令
    • vite 与 webpack 比较
    • Vite base 配置与 public 静态资源路径问题
  • 客户端数据库

    • IndexedDB 索引陷阱:字段重命名后索引返回旧数据
    • IndexedDB 事务陷阱与最佳实践:TransactionInactiveError 完全解析
  • UniApp

    • uni-app + Monorepo + 微信小程序热更新失效问题排查与结论总结
    • Monorepo + uni-app 开发中监听 / 热更新机制差异与解决方案
    • uni-app 运行与编译原理梳理
    • 为什么 uni-app 在 Monorepo 中必须做「环境区分」,而 Web 项目却不用?
  • 项目工程

    • 前端代码风格
    • Vue3 项目规范
    • npm 镜像问题
    • 包管理工具
    • 使用 engines 限制开发环境依赖
    • 打包与shell交互指定模式
    • Monorepo示例
    • PC 项目兼容 uni-app:基于 pnpm Monorepo 的工程化重构实践
    • Vue3 + Vite + pnpm Monorepo 迁移问题指南
    • 前端工程化:深入理解 npm 依赖管理机制
    • pnpm 修改依赖源码打包报错
    • 代码片段
  • 移动端

    • 浏览器跳转外部应用时请求状态码问题
  • 记录一下小程序
  • 控制浏览器正确保存网站账号密码的技巧
  • 信息泄露
  • 使用Gulp实现前端项目哈希化压缩打包
  • 前端缓存策略与文件哈希化的关联解析
  • 浏览器刷新方式与 Vue 生命周期、缓存行为解析
  • 浏览器消息铃声实现记录

Vant List 组件中双向绑定 loading 的注意事项

问题现象

在使用 Vant 的 van-list 组件时,经常会遇到 loading 状态一直为 true,导致页面一直显示加载中状态,无法正常进行分页加载的问题。

原因分析

van-list 的工作机制

van-list 的 v-model:loading 是一个双向绑定属性,它的工作流程如下:

  1. 当用户滚动到列表底部时,van-list 会自动设置 loading = true

  2. 同时触发 @load 事件

  3. 开发者在 @load 事件中处理数据加载

  4. 数据加载完成后,开发者需要手动设置 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>&nbsp;暂无数据,请先添加公告</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 事件都能正确结束并重置状态

正确理解和使用这个机制,可以避免很多常见的加载状态问题。

最后更新时间:
贡献者: 何风顺
上一页
Vue 3 Teleport 组件完全指南:从原理到实战
下一页
Vue 3 + Vite 中的路径别名与静态资源处理深度解析