Vue简易版无限加载组件实现原理与示例代码
作者:kzkz 发布时间:2024-04-28 10:54:53
背景
遇到的两个问题:scroll 事件不触发、如何将 loading 状态放在无限加载组件中进行管理。
无限加载组件在展示列表页数据时比较常见。特别是在 H5 列表页中,数据比较多,需要做分页,无限加载组件就是一个非常好的选择。
当列表页数据比较多时,一次性从服务端拿到所有的数据会比较耗时,长时间不展示列表数据,比较影响用户体验。所以对于一般的长列表数据,都会做分页。
首次请求时,只请求第一页数据;当用户上拉即将到达列表底部时,再请求下一页数据,将下一页数据拼接在之前的列表后。
mint-ui 无限加载组件体验地址:无限加载组件体验
实现功能
使用 vue3 composition API 实现如下功能:
InfinitView 组件:将 InfinitView 组件包裹在列表(项)外面即可实现无限加载。
节流加载:每次触底加载时,会自动节流,同一页数据只会请求一次(如果请求成功)。
注意:InfinitView 直接子元素高度需要比 InfinitView 组件高,才会触发滚动加载。InfinitView 组件的高度默认为其父元素的 100%。
Props
// 触底距离,当距底部距离小于等于 distance 时,会触发加载函数
distance: {
type: Number,
default: 30,
},
// 加载函数,触底时执行
onload: {
type: Function,
default: async () => {},
},
// 行内样式,在外部可以通过 classStyle 改变 InfinitView 组件的样式
classStyle: {
type: Object,
default: () => ({}),
},
使用
直接将 InfiniteView 组件包裹在列表项外面即可:
<InfiniteView :onload="onload">
<div v-for="item in list">
{{ item }}
</div>
</InfiniteView>
使用 setTimeout 模拟列表数据的加载:
// nextPage 表示下一次请求哪一页的数据
const nextPage = ref(1);
// list 表示数据列表
const list = ref(new Array(30).fill(0));
const onload = () => {
return new Promise((resolve) => {
setTimeout(() => {
list.value.push(...new Array(50).fill(0).map((_, i) => i + 1 + (nextPage.value - 1) * 50));
nextPage.value++;
resolve(nextPage.value);
}, 200);
});
}
使用时需要注意:InfinitView 组件的高度默认为其父元素的 100%,如果其父元素高度不确定(例如:由子元素撑开),会导致 InfinitView 无法监听到滚动事件,也就不会触发 onload 函数(后面会解释原因)。
解决方案有两种:
为 InfinitView 组件的父元素设置一个可计算的高度。
为 InfinitView 组件设置一个可计算的高度,可通过其 props 行内样式 classStyle 设置,或者在外部给 InfinitView 组件加上类名及其样式。
注意:这里的可计算高度可以是:由 flex 弹性容器计算得来,但不能由子元素(InfinitView)撑开得来。
组件实现
组件的实现非常简单,InfinitView 组件实际上就是一个 div,只不过在 InfinitView 内部监听了该 div 的滚动事件。在即将触底的时候去调用从父组件中传过来的 onload 函数。
其 template 实现如下:
<div
class="infinite-view"
:style="classStyle"
@scroll="onScroll($event.target)"
>
<slot />
</div>
可以通过 classStyle 在外部设置 InfinitView 组件的样式。
触发滚动事件的时候,会执行 onScroll 函数。onScroll 函数中屏蔽了调用 onload 函数的细节(触底加载、节流加载)。
使用 slot 将 InfinitView 的子组件当成该 div 的子组件。
其 style 样式如下:
.infinite-view {
height: 100%;
overflow-y: scroll;
}
InfinitView 组件的高度由其父元素决定,默认为其父元素高度的 100%,这就限制了其父元素的高度不能由 InfinitView 撑开决定。
其 script 如下:
import { ref, defineProps } from 'vue';
const props = defineProps({
distance: {
type: Number,
default: 30,
},
onload: {
type: Function,
default: async () => {},
},
classStyle: {
type: Object,
default: () => ({}),
},
});
const isloading = ref(false);
const onScroll = async (element) => {
if (isloading.value) {
return;
}
if (element.scrollHeight <= element.scrollTop + element.offsetHeight + props.distance) {
try {
isloading.value = true;
await props.onload();
isloading.value = false;
} catch (error) {
console.log(error);
isloading.value = false;
}
}
}
判断触底条件:
scrollHeight <= scrollTop + clientHeight + distance
scrollHeight 代表整个滚动区域的高度。
scrollTop 是向上滚动的距离,即从内容区顶部到整个滚动区域顶部的距离。
clientHeight 是内容区的高度。
distance 代表触底的距离,它是一个缓冲距离,即在内容区底部距离整个滚动区域底部距离小于等于 distance 时,会触发 onload 函数。
当 scrollHeight === scrollTop + clientHeight
时,刚好滑动到底部。一般情况下,我们可以提前加载,设置一个缓冲距离 distance,当即将滑动到底部,距底部不足 distance 的距离时,就可以触发加载函数。
isloading 用来控制加载的状态,并实现节流加载。
加载函数 onload 一般是异步函数,用来请求列表数据。在执行 onload 函数之前,将 isloading 设为 true,表示正在加载中;当 onload 函数执行完之后,将 isloading 设为 false,表示加载状态结束。
我们知道,scroll 事件是会频繁触发的,只要列表在滚动,onScroll 函数就会一直执行。
这就有可能导致:当滑动至距离底部不足 distance 距离时,满足触底条件,列表还在持续滚动,此时就会持续执行 onload 函数发送请求,即使上一次请求还没回来,浏览器也会持续请求同一页列表数据。
所以需要实现节流加载,控制 onload 函数的执行频率。如果上一次请求还没回来,则不执行 onload 函数。也就是在触底条件之前,如果上一次请求还在加载中,直接 return 掉。
const onScroll = async (element) => {
// 如果上一次请求还没回来,直接 return
if (isloading.value) {
return;
}
if (element.scrollHeight <= element.scrollTop + element.offsetHeight + props.distance) {
try {
// 在请求之前将 isloading 置为 true
isloading.value = true;
await props.onload();
// 请求成功之后将 isloading 置为 false
isloading.value = false;
} catch (error) {
console.log(error);
// 请求失败之后也将 isloading 置为 false
isloading.value = false;
}
}
}
实现的时候有两个细节需要提一下:scroll 事件不触发、如何将 loading 状态放在无限加载组件中进行管理。
scroll 事件
有时候经常会遇到屏幕在滚动,但是一直没有触发 scroll 事件。那是因为虽然屏幕滚动了,但是监听 scroll 事件的 div 并没有滚动。
当然我们可以省事地为 window 设置监听 scroll 的事件,不管是哪个元素触发的 scroll 事件,最终都会冒泡到 window 上面,设置的 scroll 回调函数也总是会执行。
// 在 InfinitView 中,组件挂载之后,为 window 设置监听 scroll 的事件
onMounted(() => {
window.addEventListener('scroll', (e) => {
onScroll(e.target);
})
})
上面的代码在 InfinitView 组件中,为 window 设置了监听 scroll 的事件。当屏幕滚动时,就会执行 onScroll 函数。这样也是没问题的,确实可以解决 scroll 事件不触发的问题。
但是我们并没有找到问题的根源,为什么在 InfinitView 组件中的 div 上面监听的 scroll 事件,却不会触发?
首先得知道什么情况下才会触发滚动事件
:
父元素高度比其所有子元素高度之和小。
父元素的 overflow 属性值为:
auto | scroll
。
只有满足了以上两个条件,才会触发父元素的 scroll 事件。很多时候,某个 div 的 scroll 事件没有触发,是因为我们没有设置该 div 的高度,它的高度由子元素撑开,和子元素高度之和相等。
这样即使屏幕在滚动,触发的也不是它的 scroll 事件,而是更上层 div 的 scroll 事件。例如:如果某个 div 的高度由子元素撑开,并且其父元素高度确定,比它的高度小,则在滚动的时候,不会触发该 div 的 scroll 事件,会触发它的父元素的 scroll 事件。
或者我们忘记设置监听 scroll 事件的元素的 overflow 属性了,默认情况下,overflow 的值为 visible。
$emit 发射事件和 props 回调函数的区别
我们知道在 Vue 中,子组件向父组件通信有两种方式:通过 $emit
发射事件、通过调用父组件中传过来的回调函数。
这两种方式都可以由子组件向父组件通信,但是也有一些细微的区别:
通过调用父组件中传过来的回调函数可以拿到函数的返回值,而通过
$emit
发射事件不可以。通过调用父组件中传过来的回调函数可以知道函数什么时候执行完,而通过
$emit
发射事件不可以,它只能将回调函数通过$emit
的参数,传给父组件,强迫父组件显式调用,才能在函数执行完之后做一些事情。
看起来,好像使用 props 回调函数比 $emit
发射事件要更好,那是不是 $emit
发射事件就没有好处了呢?
也不是。从名字就可以看出,$emit
发射事件,是从子组件中发射一个事件给父组件,父组件在监听到子组件发射的事件之后,可以进行一系列的操作。它只是给父组件发射一个事件,传递一个信号给父组件,父组件接收到这个信号之后,接下来要怎么做还是由父组件决定。但是使用 props 回调函数的方式则不同,它是将父组件中的一个函数通过 props 传给子组件,子组件拿到这个回调函数之后,要怎么执行,完全取决于子组件。所以子组件可以知道回调函数什么时候执行完,也可以拿到回调函数的返回值。
了解了这两种通信方式的区别,就解决了如何将 loading 状态放在无限加载组件中进行管理。
因为需要将 loading 状态放在无限加载组件(子组件)中进行管理,所以无限加载组件(子组件)必须要知道请求什么时候回来,也就是 onload 异步函数什么时候执行完。
这样我们就可以用 props 回调函数的形式,将父组件中的异步请求函数传给子组件,当列表即将滚动到底部时,将 loading 状态置为 true,然后发送请求,当请求回来之后,异步函数执行完,再将 loading 状态置为 false。如果请求没有回来,loading 状态为 true,即使再次触发了 scroll 事件,也直接返回,不再继续发送请求。
const onScroll = async (element) => {
// 如果上一次请求还没回来,直接 return
if (isloading.value) {
return;
}
if (element.scrollHeight <= element.scrollTop + element.offsetHeight + props.distance) {
try {
// 在请求之前将 isloading 置为 true
isloading.value = true;
await props.onload();
// 请求成功之后将 isloading 置为 false
isloading.value = false;
} catch (error) {
console.log(error);
// 请求失败之后也将 isloading 置为 false
isloading.value = false;
}
}
}
来源:https://juejin.cn/post/7115430717575659528


猜你喜欢
- Flask数据模型和连接数据库flask是基于MTV的结构,其中M指的就是模型,即数据模型,在项目中对应的是数据库。flask与数据库建立联
- 前言最近因为工作的原因,在做APP购物车下单支付这一块儿.被测试提了一个bug,当点加入购物车点的比较快的时候,同一个商品在购物车中出现了两
- 如何正确显示模式对话框中的中文?msg.htm <html> <head> &nbs
- package 的导入语法写 Go 代码的时经常用到 import 这个命令用来导入包,参考如下:import( "f
- 本文实例为大家分享了python openCV实现摄像头获取人脸图片的具体代码,供大家参考,具体内容如下在机器学习中,训练模型需要大量图片,
- Python离线安装包下载pip包pip download 你要下载的包名 -d 下载的路径# example 结果会下载很多whl包pip
- 前言通过辣条最近观察,大家好像对划水摸鱼是情有独钟啊。于是乎我重操旧业又写上了这么一个简单版的星空大战小游戏。当然了辣条的初衷绝对不是让你们
- 如何使用GPU而不是CPU首先查看设备from tensorflow.python.client import device_libprin
- 前言Python 提供了很多截取字符串的方法,被称为“切片(slicing)”。模版如下:strin
- 下拉菜单在实际生活中也挺常见的,它实现的js代码与tab选卡,手风琴几乎一样,在此不过多赘述。我仿照苏宁易购官网写了一个下拉菜单,实现代码如
- 前面的话正则表达式是javascript操作字符串的一个重要组成部分,但在以往的版本中并未有太多改变。然而,在ES6中,随着字符
- function commafy() { var num = document.getElementById("NumA"
- 前言最近已经播完第一季的电视剧《雪中悍刀行》,从播放量就可以看出观众对于这部剧的期待,总播放量达到50亿,可让人遗憾的是,豆瓣评分只有5.7
- 昨天群里介绍了一个专门帮你PS图片的网站。吐司网。网站在图片的预览处理上有点意思。当鼠标经过图片,显示为处理过的图片。这样大家能很清晰的对比
- 前言shape函数是Numpy中的函数,它的功能是读取矩阵的长度,比如shape[0]就是读取矩阵第一维度的长度。直接用.shape可以快速
- Tensorflow训练模型默认占满所有GPU问题在使用gpu服务器训练tensorflow模型时,总是占满显存!TensorFlow默认的
- 认为整理的还比较详细的,亲们,就快点收藏起来吧!PHP系统类函数assert函数:检查assertion声明是否错误extension_lo
- Python单元测试unittest中提供了一下四种装饰器实现测试跳过和预期故障。(使用Python 2.7.13)请查考Python手册中
- 第三章 XML的术语提纲:导言 一.XML文档的有关术语 二.DTD的有关术语导言初学XML最令人头疼的就是有一大堆新的术语概念要理解。由于
- turtle库是python的基础绘图库,官方手册这个库被介绍为一个最常用的用来给孩子们介绍编程知识的方法库,其主要是用于程序设计入门,是标