自定义HOOKS函数实现无限滚动!😍😍在现代网页开发中,无限滚动是一种常见且实用的交互模式,它能有效提升用户体验,特
在前端面试中,通常会被面试官问到你的项目有什么亮点?我们可以把无限滚动加到项目中,这样在介绍项目时更能打动面试官!无限滚动是一种常见的前端交互模式,当用户滚动页面时,页面内容会自动加载更多数据。这种方式被广泛应用于社交媒体、新闻和电子商务网站。在本文中,我们将学习如何在 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: Ref<HTMLElement | null>, // 用于监听的 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<HTMLElement | null>
, 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
来加载更多数据。
-
hasMore
是一个状态,用于指示是否还有更多的数据可以加载。 在无限滚动中,我们需要知道什么时候应该停止加载更多数据。当没有更多数据时,hasMore
设置为false
,并且我们停止对最后一个元素的观察。 -
observer.observe(newNodeRef)
:observe
方法开始观察指定的元素。 -
observer.unobserve(oldNodeRef)
:unobserve
方法停止观察指定的元素,防止资源浪费。 -
observer.disconnect()
:disconnect
方法完全停止IntersectionObserver
对所有元素的观察,并释放相关资源。当我们确认没有更多数据时,停止观察可以避免不必要的性能开销。
用 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 获取数据。
组件使用
首先是组件模板部分
<template>
<section>
<article
class="item"
v-for="(item, index) in articles"
:key="item.id"
:ref="(el) => (index === articles.length - 1 ? (itemRef = el) : '')"
>
<div>{{ index }} {{ item.title }}</div>
</article>
<div v-if="!hasMore">
没有更多数据了
</div>
</section>
</template>
v-for
指令用于遍历 articles
数组,为每个文章项生成一个 article
元素。index
是当前项的索引,item
是当前文章对象。
动态绑定 ref
来捕获最后一个元素的引用。ref
用于将 DOM 节点的引用存储到组件的一个变量中。这里使用了一个箭头函数 (el) => (index === articles.length - 1 ? (itemRef = el) : '')
,它检查当前索引是否为最后一个元素的索引,如果是,则将该元素的引用赋值给 itemRef
。这一步的目的是捕获当前渲染列表中的最后一个文章元素,后续我们会使用 itemRef
来观察这个元素的可见性。
逻辑部分
<script setup lang="ts">
import { toRefs, onMounted, ref } from 'vue';
import { useArticleStore } from './store/article';
import { useLoadMore } from './hooks/useLoadMore';
const store = useArticleStore();
const { articles, getArticles } = toRefs(store);
const currentPage = ref(1);
let hasMore = ref(true);
const itemRef = ref(null);
// 加载下一页数据
const handleNextPage = async (setHasMore) => {
currentPage.value++;
const res: any = await store.getArticles(currentPage.value);
if (!res.hasMore) {
setHasMore(false);
hasMore.value = false;
}
};
// 使用自定义 Hook
const { setHasMore } = useLoadMore(itemRef, () => {
handleNextPage(setHasMore);
});
onMounted(async () => {
await store.getArticles(currentPage.value);
});
</script>
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
监测元素的可见性,再到动态加载数据,每一步都进行了详细的解析。这种实现方式不仅简单直观,还能提升应用的性能与用户体验。如果这篇文章对你有帮助,可以点个赞哦😊!
转载自:https://juejin.cn/post/7409191765708816393