当前位置 : 主页 > 网络编程 > JavaScript >

简单方法实现Vue 无限滚动组件示例

来源:互联网 收集:自由互联 发布时间:2023-02-01
目录 1. 前言 2. 整体思路 开始 3. 钩子函数 3.1 获取偏移初始位置的像素值 3.2 获取开始滚动和结束滚动的钩子函数 4. 完整代码 1. 前言 对于列表类型的大量数据,前端展示往往采用 分页
目录
  • 1. 前言
  • 2. 整体思路
    • 开始
  • 3. 钩子函数
    • 3.1 获取偏移初始位置的像素值
    • 3.2 获取开始滚动和结束滚动的钩子函数
  • 4. 完整代码

    1. 前言

    对于列表类型的大量数据,前端展示往往采用 分页无限滚动 的方式来展示,对于用户来说,鼠标滚轮和触控屏使滚动行为要比点击更快更容易。 element-plus 组件库提供了简单的 vue 指令,就可以轻易的实现

    但是 element-plus 只支持无限向下滚动,不支持无限向上滚动,同时也没缺少丰富的 钩子函数,我们无法在这个基础上更好地 利用和改造滚动过程,所以,我们可以自己封装一个更具有个性化的组件

    2. 整体思路

    • 首先,外部的盒子会 隐藏 内部的盒子 溢出的部分
    • 内部盒子的视图展示由数据提供,每当触发向上刷新或者向下刷新的时候,及时更新数据,最好的方式是使用 数组 维护所有的数据源,触发刷新的时候,只需要 操作数组 就可以了
    • 怎么界定 向上刷新:这个很简单,只要滚动的高度接近于 0,就视作向上刷新
    • 怎么界定 向下刷新 呢?

    • scrollTop:滚动条滚动距离
    • scrollHeight:滚动条的实际高度
    • clientHeight:元素的高度

    随着滚动条滚到底部,此时 scrollTop + clientHeight = scrollHeight,那么此时就可以判断到达底部了

    开始

    const app = createApp(App);
    app.directive('infinite-scroll', {
      mounted(el, binding) {},
    });
    

    为什么使用自定义指令实现?

    自定义指令 相对于 组件 来说能够更好地操作dom元素

    • el 可以访问被添加指令的元素
    • 通过 binding.value 可以访问调用指令时传递的参数
      <ul v-infinite-scroll="{}"> // 配置参数
        <li v-for="i in dataArr">{{ i }}, len: {{ dataArr.length }}</li>
      </ul>
    

    那么我们整体结构和整体思路就有了,就不难写出如下代码:

    // 为 el 单独指定类型
    type InfiniteScrollEl = HTMLElement & {
      // 给 el 添加一个存储变量的空间
      [__SCOPE__]: {
        onScroll: () => void;
      };
    };
    interface DirectiveOpt {
      itemHeight: number; /// 内部每一列数据的高度
      rate: number; // 每次更新刷新数据的频率
      load: (dir: 'down' | 'up') => void; // 维护数据源的函数
    }
    // 获取并初始化配置选项
    const getOptions = (binding: DirectiveBinding<DirectiveOpt>) => {
      const itemHeight = binding.value.itemHeight || 60;
      const rate = binding.value.rate || 1;
      const load = binding.value.load || (() => {});
      return { itemHeight, rate, load };
    };
    app.directive('infinite-scroll', {
      async mounted(el: InfiniteScrollEl, binding: DirectiveBinding<DirectiveOpt>) {
        await nextTick(); // 确保父元素加载完毕
        const { rate, load } = getOptions(binding);
        const onScroll = () => {
          // 向下刷新
          if (el.scrollTop + el.clientHeight >= el.scrollHeight) {
            for (let i = 0; i < rate; i++) {
              load('down');
            }
          }
          // 向上刷新
          if (el.scrollTop <= 0) {
            for (let i = 0; i < rate; i++) {
              load('up');
            }
          }
        };
        el[__SCOPE__] = {
          onScroll,
        };
        el.addEventListener('scroll', onScroll);
      },
      unmounted(el) {
        const { onScroll } = el[__SCOPE__];
        el.removeEventListener('scroll', onScroll);
      },
    });
    

    在组件被完全渲染完毕的时候,立即给 el 添加滚动事件 的处理函数,同时将这个回调函数挂载在 el 上,组件被销毁的时候,删除滚动事件

    调用:

    <script lang="ts" setup>
    import { reactive } from 'vue';
    const dataArr = reactive([1, 2, 3, 4, 5]);
    let i = 0;
    const load = (dir: 'down' | 'up') => {
      if (dir === 'down') {
        dataArr.push(dataArr.length + 1);
      } else {
        dataArr.unshift(i--);
      }
    };
    </script>
    <template>
      <ul v-infinite-scroll="{ load, itemHeight: 60 }">
        <li v-for="i in dataArr">{{ i }}, len: {{ dataArr.length }}</li>
      </ul>
    </template>
    <style scoped>
    ul {
      padding: 0;
      margin: 0;
      width: 400px;
      height: 200px;
      overflow: auto;
    }
    li {
      box-sizing: border-box;
      list-style: none;
      margin-bottom: 10px;
      height: 50px;
      background-color: skyblue;
      text-align: center;
      line-height: 50px;
    }
    </style>
    

    此时会带来诸多 bug

    • 第一向下滚动的时候:此时的 scrollTop === 0,会触发向上刷新
    • 当滚动条位于顶部的时候无法向上刷新,因为这个时候 scrollTop === 0,向上滚动滚轮无法触发滚动事件

    这里的解决方案有很多种,我们采用一种比较简单易懂的方法:

    组件加载完毕的时候,给列表顶部 预留一点位置,这样不会导致第一次向上滚动无法触发滚动事件,每次向上滚动的时候,列表顶部刷新多少数据,就让列表的 scrollTop 位于 新数据与旧数据的分界处

    async mounted(el: InfiniteScrollEl, binding: DirectiveBinding<DirectiveOpt>) {
        // ...
        const { rate, load, itemHeight } = getOptions(binding);
        el.scrollTop = itemHeight; // 组件加载完毕,让列表视口顶部为第二项数据
        const onScroll = () => {
          if (el.scrollTop + el.clientHeight >= el.scrollHeight - 10) {
            for (let i = 0; i < rate; i++) {
              load('down');
            }
            await nextTick(); // 等待新增的元素加载完毕
          }
          if (el.scrollTop <= 10) {
            for (let i = 0; i < rate; i++) {
              load('up');
            }
            await nextTick(); // 等待新增的元素加载完毕
            el.scrollTop = rate * itemHeight;
          }
        };
        // ...
      },
    

    为了解决 滚动条位于顶部的时候无法向上刷新,在触发向上滚动的时候,立即改变列表 scrollTop 值,让列表视口顶部处于 刷新的数据的底部,这样就模拟了模拟了向上刷新的过程了

    此外为了防止滚动过程的卡顿,我们让刷新条件多了 10px缓冲区域

    3. 钩子函数

    3.1 获取偏移初始位置的像素值

    很多小伙伴可能会问了?这个值不就是 scrollTop?真的是这样的吗

    一旦触发向上更新,原先 scrollTop 记录的位置随着新数据的增加被 挤下来,那么新的 scrollTop 值代表的一定不是原先位置了

    我们得从 getOptions 中多添加一个处理 获取偏移值的函数,这个函数在每次 onScroll 执行完毕的时候触发

    async mounted(el: InfiniteScrollEl, binding: DirectiveBinding<DirectiveOpt>) {
        // ...
    +   const { rate, load, itemHeight, scrolledCb } = getOptions(binding);
    +   let topAddedPx = 0; // 顶部新增的高度
    +   let offset = 0; // 偏移值
        const onScroll = async () => {
          if (el.scrollTop + el.clientHeight >= el.scrollHeight - 10) {
            for (let i = 0; i < rate; i++) {
              load('down');
    +         offset = el.scrollTop - itemHeight - topAddedPx;
            }
            await nextTick(); // 等待新增的元素加载完毕
          }
          if (el.scrollTop <= 10) {
            for (let i = 0; i < rate; i++) {
              load('up');
    +         topAddedPx += itemHeight;
            }
            await nextTick(); // 等待新增的元素加载完毕
            el.scrollTop = rate * itemHeight;
          }
    +     offset = el.scrollTop - itemHeight - topAddedPx;
    +     scrolledCb(offset);
        };
        // ...
      },
    

    首先我们声明了两个变量 offset 获取实时的偏移值,topAddedPx 记录向上刷新时候新增的高度

    onScroll 结束的时候 offset 等于 此时的位置 减去 向上刷新时候新增的高度,但是别忘了还要减去 itemHeight,因为在组件初始化的时候,我们预留了一个 itemHeight 的高度

    调用:

    <script lang="ts" setup>
    // ...
    const scrolledCb = (offset: number) => {
      console.log(offset);
    };
    </script>
    <template>
      <ul v-infinite-scroll="{ load, itemHeight: 60, scrolledCb }">
        <li v-for="i in dataArr">{{ i }}, len: {{ dataArr.length }}</li>
      </ul>
    </template>
    

    3.2 获取开始滚动和结束滚动的钩子函数

    有时候,我们需要在 监听 开始滚动结束滚动 的特定时刻

    首先,在 getOptions 中多添加一个处理 获取切换滚动状态时刻的函数

      async mounted(el: InfiniteScrollEl, binding: DirectiveBinding<DirectiveOpt>) {
       // ...
       const { rate, load, itemHeight, scrolledCb, scrollingCb } = getOptions(binding);
        // ...
        let isScrolling = ref<boolean>(false); // 记录是否触发滚动
        let isNotFirst = false; // 第一次滚动不触发
        let timer: NodeJS.Timeout | null = null;
        // 监听是否滚动,如果监听值改变,立即触发滚动时刻的回调函数
        watch(isScrolling, () => {
          scrollingCb(isScrolling.value);
        });
        const onScroll = async () => {
          if (isNotFirst) isScrolling.value = true;
          isNotFirst = true;
          timer && clearTimeout(timer);
          timer = setTimeout(() => {
            isScrolling.value = false;
          }, 200);
          // ...
        };
       // ...
      },
    
    • 滑动滚轮,此时立即触发滚动的回调函数,触发 scrollingCb,并开启一个定时器,如果在 200ms 内没有再次滚动就断定为停止滚动,再次触发 scrollingCb
    • 如果在滚动开始的 200ms 内再次滑动滚轮,清空定时器,重新计时

    调用:

    <script lang="ts" setup>
    const scrollingCb = (isScrolling: boolean) => {
      console.log(isScrolling);
    };
    </script>
    <template>
      <ul v-infinite-scroll="{ load, itemHeight: 60, scrollingCb }">
        <li v-for="i in dataArr">{{ i }}, len: {{ dataArr.length }}</li>
      </ul>
    </template>
    

    4. 完整代码

    还可以设置 隐藏滚动条,让无限滚动的过程变得更加自然,只需要在使用 无限滚动指令 的元素上添加 element::-webkit-scrollbar { display: none; }

    const __SCOPE__ = 'scope';
    // 为 el 单独指定类型
    type InfiniteScrollEl = HTMLElement & {
      // 给 el 添加一个存储变量的空间
      [__SCOPE__]: {
        onScroll: () => void;
      };
    };
    interface DirectiveOpt {
      itemHeight: number; /// 内部每一列数据的高度
      rate: number; // 每次更新刷新数据的频率
      load: (dir: 'down' | 'up') => void; // 维护数据源的函数
      scrolledCb: (offset: number) => void; // 获取偏移值
      scrollingCb: (isScrolling: boolean) => void; // 获取改变滚动状态的时刻
    }
    // 获取并初始化配置选项
    const getOptions = (binding: DirectiveBinding<DirectiveOpt>) => {
      const itemHeight = binding.value.itemHeight || 60;
      const rate = binding.value.rate || 1;
      const load = binding.value.load || (() => {});
      const scrolledCb = binding.value.scrolledCb || (() => {});
      const scrollingCb = binding.value.scrollingCb || (() => {});
      return { itemHeight, rate, load, scrolledCb, scrollingCb };
    };
    app.directive('infinite-scroll', {
      async mounted(el: InfiniteScrollEl, binding: DirectiveBinding<DirectiveOpt>) {
        await nextTick();
        const { rate, load, itemHeight, scrolledCb, scrollingCb } = getOptions(binding);
        el.scrollTop = itemHeight;
        let topAddedPx = 0;
        let offset = 0;
        let isScrolling = ref<boolean>(false);
        let isNotFirst = false;
        let timer: NodeJS.Timeout | null = null;
        watch(isScrolling, () => {
          scrollingCb(isScrolling.value);
        });
        const onScroll = async () => {
          if (isNotFirst) isScrolling.value = true;
          isNotFirst = true;
          timer && clearTimeout(timer);
          timer = setTimeout(() => {
            isScrolling.value = false;
          }, 200);
          if (el.scrollTop + el.clientHeight >= el.scrollHeight - 10) {
            for (let i = 0; i < rate; i++) {
              load('down');
              offset = el.scrollTop - itemHeight - topAddedPx;
            }
            await nextTick(); // 等待新增的元素加载完毕
          }
          if (el.scrollTop <= 10) {
            for (let i = 0; i < rate; i++) {
              load('up');
              topAddedPx += itemHeight;
            }
            await nextTick(); // 等待新增的元素加载完毕
            el.scrollTop = rate * itemHeight;
          }
          offset = el.scrollTop - itemHeight - topAddedPx;
          scrolledCb(offset);
        };
        el[__SCOPE__] = {
          onScroll,
        };
        el.addEventListener('scroll', onScroll);
      },
      unmounted(el) {
        const { onScroll } = el[__SCOPE__];
        el.removeEventListener('scroll', onScroll);
      },
    });

    以上就是简单方法实现Vue 无限滚动组件示例的详细内容,更多关于Vue 无限滚动组件的资料请关注自由互联其它相关文章!

    上一篇:vue选项卡Tabs组件实现示例详解
    下一篇:没有了
    网友评论