• 作者:老汪软件技巧
  • 发表时间:2024-09-02 04:02
  • 浏览量:

在前端面试中,通常会被面试官问到你的项目有什么亮点?我们可以把无限滚动加到项目中,这样在介绍项目时更能打动面试官!无限滚动是一种常见的前端交互模式,当用户滚动页面时,页面内容会自动加载更多数据。这种方式被广泛应用于社交媒体、新闻和电子商务网站。在本文中,我们将学习如何在 Vue 3 中使用 IntersectionObserver API 编写一个自定义 Hook,实现无限滚动加载更多数据的功能。

实现思路

我们将通过以下几个步骤来实现无限滚动:

使用 IntersectionObserver API 监听最后一个数据项:IntersectionObserver 是一种浏览器提供的 API,用于异步观察目标元素与其祖先元素(通常是视口)交集的变化。当我们滚动页面,目标元素进入或离开视口时,IntersectionObserver 会触发回调函数。在无限滚动中,我们可以监听列表中最后一个元素,当该元素进入视口时,触发加载更多数据的操作。

编写自定义 Hook:在 Vue 3 中,我们可以利用 ref 和 watch 等功能封装一个自定义 Hook,用来管理 IntersectionObserver 的逻辑,这样可以让这个功能在多个组件中重复使用。

将自定义 Hook 集成到 Vue 组件中:在组件中,我们通过调用这个自定义 Hook 来监听最后一个数据项,并结合 Vuex 或 Pinia 来管理和加载数据。

编写自定义 Hook:useLoadMore

useLoadMore 的代码实现

import { ref, watch } from 'vue';
import type { Ref } from 'vue';
export const useLoadMore = (
    nodeRef: Refnull>,  // 用于监听的 DOM 元素的引用
    loadMore: () => void                // 加载更多数据的回调函数
) => {
    let observer: IntersectionObserver | null = null;
    const hasMore = ref(true); // 是否有更多数据的标志
    // 监听节点引用的变化
    watch(nodeRef, (newNodeRef, oldNodeRef) => {
        // 如果之前已经存在 observer,则取消对旧元素的观察
        if (oldNodeRef && observer) {
            observer.unobserve(oldNodeRef);
        }
        // 如果新节点存在,初始化 IntersectionObserver
        if (newNodeRef) {
            observer = new IntersectionObserver(([entry]) => {
                if (entry.isIntersecting) { // 当元素进入视口时
                    loadMore(); // 触发加载更多数据的函数
                }
            });
            observer.observe(newNodeRef); // 开始观察新的节点
        }
    });
    // 监听 hasMore 的变化,当没有更多数据时停止观察
    watch(hasMore, (value) => {
        if (observer) {
            if (value && nodeRef.value) {
                observer.observe(nodeRef.value); // 继续观察
            } else {
                observer.disconnect(); // 停止观察并释放 observer 对象
            }
        }
    });
    return {
        hasMore,
        setHasMore: (value: boolean) => {
            hasMore.value = value; // 手动设置是否有更多数据
        }
    };
};

首先,定义传入节点, nodeRef: Ref, nodeRef 是传入的节点引用,指向我们想要观察的 DOM 元素(通常是列表中的最后一个元素)。这个引用可以是一个 ref,指向一个 DOM 节点。通过 nodeRef,我们能够在 Vue 组件中获取到具体的 DOM 元素,然后使用这个元素来初始化 IntersectionObserver。

然后我们定义一个回调函数 loadMore ,主要的功能就是当监听的元素进入视口时,这个函数会被触发,用来加载更多的数据。其主要是通过watch 去监听节点变化。

在 watch(nodeRef, (newNodeRef, oldNodeRef) => { ... }) 中,watch 用于监听 nodeRef 的变化,确保每当 nodeRef 指向的元素发生变化时,我们能够及时地停止对旧元素的观察,并开始观察新元素。当 nodeRef 指向的新元素与旧元素不同,我们首先取消对旧元素的观察(observer.unobserve),然后开始观察新的元素(observer.observe)。

在IntersectionObserver(([entry]) => { ... })中,entry 是一个 IntersectionObserverEntry 对象的数组,每个对象都包含了被观察元素的可见性信息。如果 entry.isIntersecting 为 true,说明元素进入了视口,此时我们调用 loadMore 来加载更多数据。

用 Pinia 管理文章列表

我们使用 Pinia 来管理文章数据,并实现分页加载。下面是 Pinia Store 的实现代码:

import { defineStore } from "pinia";
import type { Article } from '../types/article';
import { ref } from 'vue';
export const useArticleStore = defineStore('article', () => {
    const _articles: Article[] = [
        // 假设这里有一组初始的文章数据
    ];
    const articles = ref<Article[]>([]);
    
    // 加载更多文章的函数,模拟分页加载
    const getArticles = (page: number, size: number = 10) => {
        return new Promise((resolve) => {
            setTimeout(() => {
                const data = _articles.slice((page - 1) * size, page * size);
                articles.value = [...articles.value, ...data];
                resolve({
                    data,
                    page,
                    total: _articles.length,
                    hasMore: page * size < _articles.length
                });
            }, 500);
        });
    };
    return {
        articles,
        getArticles
    };
});

滚动的定义__无限滚动列表实现

_articles 是一个包含初始文章数据的数组,它作为模拟数据源,提供给后续分页加载使用。

articles 是一个 ref,它是存储在 Store 中的响应式状态,用于保存已加载的文章数据。通过使用 ref,当 articles 的值发生变化时,绑定到它的 Vue 组件会自动重新渲染。articles 初始化为空数组,随着数据的加载,新的数据会追加到这个数组中。需要区分这两个变量的不同,_articles是全部的文章数据的数组,而articles是不断获取_articles的文章数据,需要在Vue组件渲染的数据。

接着再定义一个getArticles方法 ,用于模拟分页加载文章数据,并将新的数据追加到 articles 中。slice 方法通过 page 和 size 参数,从 _articles 中提取一部分数据,模拟分页的效果。再头通过articles.value = [...articles.value, ...data]; 将新获取的数据追加到现有的 articles 数组中。

需要注意的是为了模拟网络请求的延迟,这里使用了 setTimeout 包裹在 Promise 中,延迟 500 毫秒后返回数据。这也展示了如何在真实的应用中处理异步操作,例如从 API 获取数据。

组件使用首先是组件模板部分


v-for 指令用于遍历 articles 数组,为每个文章项生成一个 article 元素。index 是当前项的索引,item 是当前文章对象。

动态绑定 ref 来捕获最后一个元素的引用。ref 用于将 DOM 节点的引用存储到组件的一个变量中。这里使用了一个箭头函数 (el) => (index === articles.length - 1 ? (itemRef = el) : ''),它检查当前索引是否为最后一个元素的索引,如果是,则将该元素的引用赋值给 itemRef。这一步的目的是捕获当前渲染列表中的最后一个文章元素,后续我们会使用 itemRef 来观察这个元素的可见性。

逻辑部分


currentPage 表示当前加载的页码,hasMore 是一个布尔值,用来标识是否还有更多数据可加载。itemRef 用于存储当前正在观察的 DOM 元素引用即最后一个文章项。

这里的handleNextPage 函数用于加载下一页的数据。每次调用时,它会增加 currentPage 的值,并通过 store.getArticles 获取下一页的数据。如果返回的结果中 hasMore 为 false,则表示没有更多数据可加载,函数会调用 setHasMore(false) 来更新状态。

通过 useLoadMore(itemRef, () => { handleNextPage(setHasMore); }); 调用自定义的 Hook。用于在指定元素(itemRef)进入视口时,触发回调函数(即 handleNextPage)。useLoadMore 返回一个 setHasMore 函数,用于在需要时更新 hasMore 的状态。

需要注意的是在组件初始化时,我们需要加载第一页的文章数据,这里通过 await store.getArticles(currentPage.value); 实现。

效果演示

总结

通过本文的讲解,我们学习了如何利用 Vue 3 的组合式 API 和自定义 Hooks 来实现无限滚动功能。从捕获 DOM 元素的引用到使用 IntersectionObserver 监测元素的可见性,再到动态加载数据,每一步都进行了详细的解析。这种实现方式不仅简单直观,还能提升应用的性能与用户体验。如果这篇文章对你有帮助,可以点个赞哦!


上一条 查看详情 +没有了
下一条 查看详情 +没有了