Code前端首页关于Code前端联系我们

一、先搞懂,Vue3 无限滚动是啥,适合啥场景?

terry 2小时前 阅读数 7 #Vue
文章标签 Vue3;无限滚动

p>做前端开发时,碰到那种数据巨多的列表,比如刷朋友圈一样的动态流、电商APP里的商品列表,要是一次性把数据全加载出来,页面准卡成PPT,这时候「无限滚动」就派上大用场了——用户滑到列表底部,自动加载下一页内容,既流畅又省资源,但Vue3 环境下咋实现这功能?有没有简单好上手的方法?这篇从原理到实战,把无限滚动的门道拆明白。

“无限滚动”本质是让列表数据“按需加载”:页面初始只加载第一页,用户滑动到列表底部附近时,自动发起请求加载下一页,把新数据拼到列表末尾,这种交互在很多场景里常见:

  • 社交类App的动态流(像微博、朋友圈,一直滑一直加载新动态);
  • 电商平台的商品列表(比如淘宝搜索页,滑到底加载更多商品);
  • 资讯类平台的文章流(知乎、头条的信息流);
  • 瀑布流布局(比如图片墙,多列排版,每列滑到底加载)。

对比传统“分页按钮”,无限滚动优势很明显:用户不用点“下一页”,全程沉浸式浏览,交互更自然;同时减少前端一次性渲染的DOM数量,页面更流畅,但它也有缺点,比如用户想回翻历史内容时不太方便(不过大部分场景用户更在意“刷新内容”,所以利大于弊)。

实现无限滚动的核心原理是啥?

不管用原生方法还是第三方库,核心逻辑都是“判断用户是否滑到了列表底部(或底部附近)”,然后触发下一页请求,常见有两种技术思路:

监听滚动事件(scroll)

给滚动容器(比如<div>window)绑定scroll事件,每次滚动时计算三个关键数值:

  • scrollTop:容器滚动条卷上去的高度;
  • scrollHeight:容器内部内容的总高度;
  • clientHeight:容器本身的可见高度。

scrollTop + clientHeight ≥ scrollHeight - 阈值 时(比如阈值设为200px,提前加载下一页),就触发加载逻辑。

但这种方式有个问题:scroll事件触发频率极高(用户滑动时会疯狂触发),如果直接在事件里发请求,容易导致性能爆炸或重复请求,所以得用节流(throttle)或防抖(debounce) 控制触发频率,比如1秒内只触发一次。

Intersection Observer API(更高效)

这是浏览器提供的“观察者”API,能监听目标元素是否进入(或离开)视口,实现思路是:在列表底部放一个“哨兵”元素(比如<div class="sentinel"></div>),当这个哨兵进入视口时,说明用户快滑到底了,触发加载。

优点很明显:不用频繁计算滚动数值,浏览器底层优化了性能,哪怕列表很长也不会卡;而且能灵活配置“触发加载的时机”(比如哨兵距离底部还有50px时就触发,通过rootMargin设置)。

Vue3 里用原生方法咋实现无限滚动?

先从最基础的“自己写逻辑”开始,步骤清晰,理解原理后再用库更顺手。

步骤1:搭建滚动容器和列表结构

假设做一个“用户列表”,每次加载10条用户数据,先写模板:

<template>
  <div class="scroll-container" ref="scrollRef">
    <ul>
      <li v-for="user in userList" :key="user.id">{{ user.name }}</li>
    </ul>
    <!-- 加载中提示 -->
    <div v-if="isLoading" class="loading">加载中...</div>
    <!-- 哨兵元素(用Intersection Observer时需要) -->
    <div ref="sentinelRef" class="sentinel"></div>
  </div>
</template>

给滚动容器.scroll-container设置固定高度和overflow-auto,让它自己滚动:

.scroll-container {
  height: 500px;
  overflow-y: auto;
}

步骤2:用scroll事件实现(传统方式)

在Vue3的<script setup>里写逻辑:

<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import { debounce } from 'lodash' // 引入节流防抖,也可以自己实现
const userList = ref([])
const isLoading = ref(false)
const hasMore = ref(true) // 是否还有更多数据
const page = ref(1)
const scrollRef = ref(null)
// 模拟接口请求
const fetchData = async () => {
  isLoading.value = true
  // 这里替换成真实接口,axios.get(`/api/users?page=${page.value}`)
  const mockData = Array(10).fill(0).map((_, i) => ({
    id: page.value * 10 + i,
    name: `用户${page.value * 10 + i}`
  }))
  userList.value = [...userList.value, ...mockData]
  page.value++
  isLoading.value = false
  // 假设最多5页数据,模拟没有更多的情况
  if (page.value > 5) hasMore.value = false
}
// 滚动事件处理函数(加防抖)
const handleScroll = debounce(() => {
  if (!scrollRef.value || isLoading.value || !hasMore.value) return
  const { scrollTop, scrollHeight, clientHeight } = scrollRef.value
  // 距离底部小于200px时触发加载
  if (scrollTop + clientHeight >= scrollHeight - 200) {
    fetchData()
  }
}, 200) // 200毫秒内只触发一次
onMounted(() => {
  fetchData() // 初始加载第一页
  scrollRef.value?.addEventListener('scroll', handleScroll)
})
onUnmounted(() => {
  scrollRef.value?.removeEventListener('scroll', handleScroll)
})
</script>

关键点解释:

  • debouncehandleScroll包一层,200毫秒内只执行一次,避免scroll事件疯狂触发;
  • isLoading锁,防止上一次请求没完成就发新请求;
  • hasMore控制是否还有数据,没有的话不再触发加载。

步骤3:用Intersection Observer实现(更优雅)

scroll事件换成Intersection Observer,代码更简洁,性能更好:

<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
const userList = ref([])
const isLoading = ref(false)
const hasMore = ref(true)
const page = ref(1)
const sentinelRef = ref(null) // 哨兵元素的ref
let observer = null
const fetchData = async () => {
  // 和之前一样,模拟请求...
}
onMounted(() => {
  fetchData()
  // 创建观察者
  observer = new IntersectionObserver((entries) => {
    // entries是被观察元素的状态数组,这里只有sentinel一个元素
    const target = entries[0]
    if (target.isIntersecting && hasMore.value && !isLoading.value) {
      // 哨兵进入视口,触发加载
      fetchData()
    }
  }, {
    root: null, // 视口作为根元素(如果滚动容器是window)
    rootMargin: '200px', // 哨兵距离视口底部200px时就触发(提前加载)
    threshold: 0 // 只要有1px进入视口就触发
  })
  // 监听哨兵元素
  observer.observe(sentinelRef.value)
})
onUnmounted(() => {
  observer?.unobserve(sentinelRef.value) // 销毁观察者
})
</script>

这种方式不需要计算滚动数值,浏览器自动帮我们判断元素是否可见,代码更简洁,性能也更好,如果滚动容器不是window(比如前面的.scroll-container),需要把root改成滚动容器的ref

root: scrollRef.value, // 滚动容器作为根元素

用第三方库简化开发,有哪些选择?

自己写逻辑灵活,但项目里重复开发太麻烦,Vue3生态里有不少成熟的无限滚动库,挑几个常用的讲:

vue-infinite-scroll-next(Vue3 兼容版)

这是对经典库vue-infinite-scroll的Vue3适配版,用指令式开发,很方便。

安装:

npm i vue-infinite-scroll-next

全局注册(main.js):

import { createApp } from 'vue'
import App from './App.vue'
import InfiniteScroll from 'vue-infinite-scroll-next'
const app = createApp(App)
app.use(InfiniteScroll)
app.mount('#app')

组件内使用:

<template>
  <div v-infinite-scroll="fetchData" 
       infinite-scroll-disabled="isLoading || !hasMore"
       infinite-scroll-distance="200"
       class="scroll-container">
    <ul>
      <li v-for="user in userList" :key="user.id">{{ user.name }}</li>
    </ul>
    <div v-if="isLoading">加载中...</div>
  </div>
</template>
<script setup>
import { ref } from 'vue'
// ...fetchData、userList等逻辑和之前一样...
</script>

指令参数解释:

  • v-infinite-scroll:绑定触发的加载函数;
  • infinite-scroll-disabled:禁用加载的条件(加载中或没有更多数据时禁用);
  • infinite-scroll-distance:距离底部多少像素时触发(类似之前的阈值)。

element-plus 的 InfiniteScroll 指令

如果项目用了Element Plus UI库,它内置了InfiniteScroll指令,不用额外装库。

使用示例:

<template>
  <el-scrollbar style="height: 500px;">
    <ul v-infinite-scroll="fetchData" 
        infinite-scroll-disabled="isLoading || !hasMore"
        infinite-scroll-distance="200">
      <li v-for="user in userList" :key="user.id">{{ user.name }}</li>
    </ul>
    <div v-if="isLoading" class="el-loading-spinner">加载中...</div>
  </el-scrollbar>
</template>

Element Plus的指令和vue-infinite-scroll-next用法类似,好处是和UI库风格统一,自带加载动画,适合快速开发。

vue3-infinite-scroll(轻量库)

另一个专注Vue3的库,API更灵活,支持自定义滚动容器、加载状态等,安装和使用看官方文档,核心思路和前两个库类似,都是指令式封装,减少重复代码。

进阶:性能优化和特殊场景处理

无限滚动看似简单,碰到复杂场景(比如上万条数据、瀑布流),不优化会巨卡,分享几个进阶技巧:

结合虚拟滚动(解决长列表DOM爆炸)

无限滚动解决“按需加载数据”,但如果加载了1万条数据,DOM节点太多还是会卡死,这时候要结合虚拟滚动:只渲染用户当前能看到的区域的DOM,其他区域用空白占位。

推荐库:vue-virtual-scroller(Vue3支持),用法是把列表换成<VirtualScroller>组件,它内部自动处理DOM渲染,结合无限滚动的话,在虚拟滚动的容器里监听加载,数据还是按需请求,但DOM始终只渲染几十条,性能起飞。

瀑布流布局的无限滚动

瀑布流是多列布局(比如两列图片,高度不一),这时候不能只监听整个容器的滚动,得给每一列加哨兵元素,或者计算每一列的底部位置。

思路:每一列是一个滚动容器(或弹性布局的列),在每列底部放哨兵,用Intersection Observer监听,当任意一列的哨兵进入视口时,加载下一页数据,再把新数据分配到最短的列里(保持瀑布流布局)。

处理异步请求的竞态问题

比如用户快速滑动,导致连续发了多个请求,旧请求比新请求晚返回,会把新数据冲掉(比如第一页请求延迟,第三页请求先回来,第一页再回来就会把第三页数据覆盖)。

解决方法:

  • AbortController取消旧请求(axios等库支持);
  • 维护一个“最新请求标识”,只有最新的请求返回才更新数据;
  • isLoading锁,确保同一时间只有一个请求在飞。

移动端适配

移动端和PC端的滚动逻辑差不多,因为移动端浏览器的滚动也会触发scroll事件,但要注意:

  • 有些手机浏览器有“回弹”效果,导致scroll事件触发时机略有不同,测试时多刷几遍;
  • 如果用了overflow: auto的滚动容器,在移动端可能需要加-webkit-overflow-scrolling: touch让滚动更流畅。

常见问题排坑指南

实际开发中总会碰到奇奇怪怪的问题,分享几个高频坑和解决方法:

滚动容器不是window,监听失效

如果滚动容器是自己写的<div class="scroll-container">,一定要确保给这个容器加ref,并监听它的scroll事件(而不是window),用Intersection Observer时,也要把root设为这个容器的ref

首次加载和滚动加载逻辑分离

有些场景需要“初始加载第一页”,之后滚动加载,这时候要在onMounted里先调fetchData(),再绑定滚动监听,如果用第三方库,注意infinite-scroll-immediate参数(是否立即触发加载,默认是true,可能导致初始加载两次,要根据需求关闭)。

没有更多数据后,仍然触发加载

要在fetchData里判断hasMore,当数据加载完(比如接口返回totalPage等于当前页),把hasMore.value = false,同时在禁用条件里加上!hasMore,比如infinite-scroll-disabled="isLoading || !hasMore"

节流防抖没做好,重复请求

检查节流防抖的逻辑:比如用debounce时,是否正确包裹了事件处理函数;阈值(比如200毫秒)是否合理(太大会延迟加载,太小会重复触发),可以在handleScroll里打日志,看触发频率是否正常。

Intersection Observer不生效

检查这几点:

  • 哨兵元素是否真的在列表底部(有没有被其他元素挡住,或者CSS设置了display: none);
  • root参数是否正确(滚动容器不是window时,要设为容器的ref);
  • rootMargin的单位是否正确(比如'200px',不能少了单位);
  • 观察者是否在组件销毁时取消监听(否则可能内存泄漏,或者重复触发)。

Vue3实现无限滚动,核心是“判断触底时机 + 异步加载数据”,新手可以从原生scroll事件或Intersection Observer入手,理解原理后,用第三方库(如vue-infinite-scroll-next、Element Plus指令)能大幅提效,碰到性能问题,结合虚拟滚动、瀑布流处理等进阶技巧,能cover绝大多数场景。

关键是先理清楚触发条件(什么时候加载)、控制加载状态(避免重复请求)、处理边界情况(没有更多数据、容器不是window),多测试不同设备和网络环境,无限滚动的体验才能丝滑~

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

热门