Vue简易版无限加载组件实现原理与示例代码

目录
  • 背景
  • 实现功能
    • Props
    • 使用
    • 组件实现
  • scroll 事件
  • $emit 发射事件和 props 回调函数的区别
  • 总结

背景

遇到的两个问题:scroll 事件不触发、如何将 loading 状态放在无限加载组件中进行管理。

无限加载组件在展示列表页数据时比较常见。特别是在 H5 列表页中,数据比较多,需要做分页,无限加载组件就是一个非常好的选择。

当列表页数据比较多时,一次性从服务端拿到所有的数据会比较耗时,长时间不展示列表数据,比较影响用户体验。所以对于一般的长列表数据,都会做分页。

首次请求时,只请求第一页数据;当用户上拉即将到达列表底部时,再请求下一页数据,将下一页数据拼接在之前的列表后。

实现功能

使用 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;
    }
  }
}

总结

到此这篇关于Vue简易版无限加载组件实现原理的文章就介绍到这了,更多相关Vue无限加载组件实现内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Vue2组件tree实现无限级树形菜单

    一直打算偷懒使用个现成的树组件,但是在github上找了一大圈没有找到真正满足应用开发的树组件,所以没办法只能自己写了一个,开源出来希望可以帮助到需要的人,同时如果大家觉得好用,我可以顺带骗骗★(希望喜欢的朋友对我体力的肯定可以点下★ ),由于我也算刚接触vue,所以难免有所考虑不周的地方,希望大家在issue里面指正.组件重点是父子组件数据的共享和状态保持,我是利用了下vuex的思路,采用一个控制仓库完成. github 地址 vue-tree How to run demo npm inst

  • Vue.js组件tree实现无限级树形菜单

    分享一段用 <ul>和<li>标签实现tree的代码,可能写的不是很好,如果大家有更好的希望分享下. 代码看这里喽: html代码: <div class="tree"> <nav class='navbar'> <ul class='nav nav-stacked'> <template v-for='item in menus'> <li role='presentation' v-if='!item.c

  • vue单个组件实现无限层级多选菜单功能

    wTree.vue  原理:每一个多选框都是一个节点,每个节点就是一个wTree组件,有父级(顶级level为0),有子级(底层list[]是空的),组件之间状态传递是通过组件通信传递,对于外部数据checkList数组的修改是通过store实现的.初始化从底层状态传递到上层,一层一层传递.改变状态,不同状态改变,修改checklist数组.大概就这个思路,下面是代码: <template> <div> <div > <span v-for="o in

  • Vue简易版无限加载组件实现原理与示例代码

    目录 背景 实现功能 Props 使用 组件实现 scroll 事件 $emit 发射事件和 props 回调函数的区别 总结 背景 遇到的两个问题:scroll 事件不触发.如何将 loading 状态放在无限加载组件中进行管理. 无限加载组件在展示列表页数据时比较常见.特别是在 H5 列表页中,数据比较多,需要做分页,无限加载组件就是一个非常好的选择. 当列表页数据比较多时,一次性从服务端拿到所有的数据会比较耗时,长时间不展示列表数据,比较影响用户体验.所以对于一般的长列表数据,都会做分页.

  • Vue中的无限加载vue-infinite-loading的方法

    本文介绍了Vue中的无限加载vue-infinite-loading的方法,分享给大家,具体如下: 注意:vue-infinite-loading2.0只能在Vue.js2.0中使用.如果你想在Vue.js1.0中使用,请安装vue-infinite-loading1.3版本 如何安装 npm install vue-infinite-loading --save 导入方式 es6模块导入方式 import InfiniteLoading from 'vue-infinite-loading';

  • Vue实现一个无限加载列表功能

    一个需要判断的地方就是加载中再次触发滚动的时候,不要获取数据. <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>列表无限加载</title> <style> * { margin: 0; padding: 0; } li { height: 50px; border-bottom: 1px s

  • vue实现按需加载组件及异步组件功能

    说实话,我一开始也不知道什么叫按需加载组件,组件还可以按需加载???后来知道了 学不完啊...没关系,看我的 按需加载组件,或者异步组件,主要是应用了component的 is 属性 template中的代码: 这里的每一个按钮,都要显示不同的组件,所以我让他们使用了同一个方法名 <template slot-scope="scope"> <el-button type="text" size="mini" @click=&qu

  • vue实现路由懒加载的3种方法示例

    前言 路由懒加载在访问页面的时候非常重要,能够提高首页加载速度,避免出现加载时候白页,如果没有懒加载,webpack打包后的文件会非常大. import按需加载(常用) vue异步组件 webpack提供的require.ensure() 1.import按需加载(常用) 允许将不同的组件打包到一个异步块中,需指定了相同的webpackChunkName. 把组件按组分块 const A = () => import(/* webpackChunkName: "group-A"

  • Python 保存加载mat格式文件的示例代码

    mat为matlab常用存储数据的文件格式,python的scipy.io模块中包含保存和加载mat格式文件的API,使用极其简单,不再赘述:另附简易示例如下: # -*- coding: utf-8 -*- import numpy as np import scipy.io as scio # data data = np.array([1,2,3]) data2 = np.array([4,5,6]) # save mat (data format: dict) scio.savemat(

  • Vue.js实现无限加载与分页功能开发

    本篇文章是一篇Vue.js的教程,目标在于用一种常见的业务场景--分页/无限加载,帮助读者更好的理解Vue.js中的一些设计思想.与许多Todo List类的入门教程相比,更全面的展示使用Vue.js完成一个需求的思考过程:与一些构建大型应用的高阶教程相比,又更专注于一些零碎细节的实现,方便读者快速掌握.致用. 需求分析 当一个页面中信息量过大时(例如一个新闻列表中有200条新闻需要展示),就会产生问题,例如: >数据量过大,影响加载速度 >用户体验差,很难定位到之前自己看过的某篇文章 >

  • Vue.js上下滚动加载组件的实例代码

    由于工作的需要并鉴于网上的vue.js滚动加载方案不合适,自己写了一个简单实用的.就短短的150行代码. 组件代码 // scrollLoader.vue // 滚动加载组件 <style scoped> .container-main {margin: 0 auto; overflow: auto; overflow-x: hidden; padding: 0;} .loading{ width: 100%; height: 40px; position: relative; overflo

  • vue基于vant实现上拉加载下拉刷新的示例代码

    前言 普遍存在于各种app中的上拉加载下拉刷新功能大家都不陌生吧,一般来说,在数据量比较大的情况下,为了更快的渲染和给用户更好的观感体验,我们会将数据做分页处理,让其批量加载,这样一来,在渲染速度上,能给用户一个比较好的体验效果.话说回来,分页处理,也就是我们今天要讲的上拉加载和下拉刷新. 实现思路 下拉刷新: 请求接口赋完值后,将接口返回数据长度与总条数进行比较控制加载不加载的状态,在下拉刷新方法中定义起始页码为第一页,调整加载的状态为false,最后调用请求数据的接口方法,做适当轻提示即可.

  • 动态加载dtree.js树treeview(示例代码)

    复制代码 代码如下: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"><html><head><title>Destroydrop » Javascripts » Tree</title><link rel="StyleSh

随机推荐