My Blog

无限滚动与 van-list 实践

·前端·2 分钟阅读
Vant无限滚动

Vant van-list 组件实现的无限滚动方案,采用 scroll 事件 + getBoundingClientRect()距离计算(非 Intersection Observer)。

[!info] 源码确认 以下原理基于 Vant 源码 packages/vant/src/list/List.tsx分析。

原理

van-list 在列表底部放置一个 placeholder div 作为哨兵元素,通过简童滚动容器的 scroll 事件,在每次滚动时计算哨兵与容器底边的距离,当距离小于 offset(默认 300px)时触发加载。

用户滚动 → scroll 事件触发 → 计算 placeholder 底边与容器底边距离 → 距离 <= offset → 触发 load

核心检测逻辑(根据源码简化):

const check = () => {
  if (loading.value || finished.value || disabled.value) return;

  const scrollParentRect = getBoundingClientRect(scroller);
  const placeholderRect = getBoundingClientRect(placeholder);

  // 向下滚动:哨兵底边距容器底边的距离  <= offset
  const isReachEage = placeholderRect.bottom - scrollParentRect.bottom <= offset;

  if (isReachEage) {
    loading.value = true;
    emit('load');
  }
};

滚动监听使用 passive 模式性能优化:

useEventListener('scroll', check, {
  target: scroller,
  passive: true,
});

与Intersection Observer的关系

van-list 没有使用 Intersection Observer,而是选择了传统 scroll 事件方案。两者对比如下:

维度van-list(scroll+rect)Intersection Observer方案
触发频率每次scroll事件都计算只在超过阈值时回调
性能passive listener缓解,但仍有开销浏览器内部优化,更高效
预加载通过offset(300px)提前触发通过 rootMargin 实现类似效果
兼容性全浏览器兼容2019+全兼容
实现复杂度较简单直接需管理 observer 生命周期

van-list核心属性

属性类型说明
v-model:loadingBoolean加载状态,true 时显示 loading 且不重复触发
finishedBoolean是否已加载完全部数据
finished-textString加载完毕提示文案
immediate-checkBoolean初始化时是否立即检查(常设为 false 防止首屏重复加载
offsetNumber触发加载的距离阈值,默认300px
directionString触发滚动方向,down(默认)或up
@loadEvent触发加载的回调

标准实现模式

<van-list
  v-model:loading="loading"
  :finished="finished"
  finished-text="没有更多了"
  :immediate-check="false"
  @load="onLoad"
>
  <div v-for="item in list" :key="item.id">
    {{ item.name }}
  </div>
</van-list>
const list = ref([]);
const loading = ref(false);
const finished = ref(false);
const page = ref(1);
const pageSize = 10;

function onLoad() {
  API.getList({ pageNumber: page.value++, pageSize })
    .then(res => {
      list.value.push(...res.datas);
      loading.value = false;
      if (page.value > Math.ceil(res.total / pageSize)) {
        finished.value = true;
      }
    })
    .catch(() => { loading.value = false; });
}

下拉刷新集成

配合 van-pull-refresh实现下拉刷新 + 无限滚动的完整体验:

<van-list v-model:loading="loading" :finished="finished" @load="onLoad">
  <van-pull-refresh v-model="refreshing" @refresh="onRefresh">
    <!-- 列表内容 -->
  </van-pull-refresh>
</van-list>
function onRefresh() {
  finished.value = false;
  refreshing.value = true;
  loading.value = true;
  onLoad();
}

// onLoad 中检测 refreshing 重置状态
function onLoad() {
  if (refreshing.value) {
    list.value = [];
    refreshing.value = false;
    page.value = 1;
  }
  // ...正常分页逻辑
}

实战要点

  • 防重复加载loading = true期间 van-list 不会再次触发 load
  • 防切换竞态: 切换 tab 时用before-change拦截,确保当前加载完成
  • 滚动位置保持: 配合onActivated / useScroll在 keep-alive 缓存中恢复位置
  • 结束判断: page >= Math.ceil(total / pageSize)时标记finished = true
  • 错误处理: catch中必须loading = false,否则列表卡死无法重试
  • 预加载距离: 通过offset属性调整,默认 300px,值越大越早触发下一页加载

相关文章