louzhedong/blog

Vue中滚动加载更多的实现

louzhedong opened this issue · 0 comments

滚动加载是前端一个非常常见的场景,本文来实现一个基于Vue的滚动加载的功能

知识点:

  1. 加载更多主要是通过对滚动元素滚动距离的判断,来决定加载的时机
  2. Vue.directive 可以给 DOM 元素增加自定义指令,更方便地将滚动事件监听到对应的元素上

实现

  1. 获取滚动元素容器的高度

    function getClientHeight(element) {
      return element.clientHeight;
    }
  2. 获取滚动元素滚动范围的高度

    function getScrollHeight(element) {
      return element.scrollHeight;
    }
  3. 获取滚动元素已经滚动的高度

    function getScrollTop(element) {
      return element.scrollTop;
    }
  4. 决定是否执行监听函数的判断

    假设我们决定当页面滚动到里底部20距离的时候出发我们的监听函数,可以这样进行判断

    if (scrollHeight - scrollTop - clientHeight < 20) {
        callback();
      }
  5. Vue 自定义指令的功能

    • bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。

    • inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。

    • update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新

    • componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用。

    • unbind:只调用一次,指令与元素解绑时调用。

    在本例中,我们使用bind和unbind钩子函数

  6. 滚动事件是一个出发频率非常高的方法,需要使用节流函数来控制其频率

代码

// utils.js
export function throttle(fn, delay = 200) {
  let lastDate = 0; // 上次执行事件

  return function () {
    const context = this, argument = arguments;
    const now = +new Date();
    if (now - lastDate > delay) {
      lastDate = now;
      fn.apply(context, argument);
    }
  }
}


// InfiniteScroll.js
import { throttle } from './utils';
const DEAULT_DISTANCE = 20;

function getClientHeight(element) {
  return element.clientHeight;
}

function getScrollHeight(element) {
  return element.scrollHeight;
}

function getScrollTop(element) {
  return element.scrollTop;
}

function getScrollElement(element) {
  if (element === window) {
    return window;
  }
  let currentElement = element;

  const overflowY = document.defaultView.getComputedStyle(currentElement).overflowY;
  if (overflowY === 'scroll' || overflowY === 'auto') {
    return currentElement;
  }
  return getScrollElement(element.parentNode);
}

function scrollEvent(element, callback, vnode) {
  const loadingAttr = vnode.data.attrs.loading;

  if (loadingAttr) {
    return;
  }
  const clientHeight = getClientHeight(element);
  const scrollHeight = getScrollHeight(element);
  const scrollTop = getScrollTop(element);
  let distance = DEAULT_DISTANCE;
  const distanceAttr = vnode.data.attrs.distance;

  if (typeof distanceAttr !== undefined) {
    distance = Number(distanceAttr);
  }

  if (scrollHeight - scrollTop - clientHeight < distance) {
    callback();
  }
}

function bindEvent(el, binding, vnode) {
  const cb = binding.value;
  const throttleCb = throttle(cb, 200);
  const element = getScrollElement(el);

  /**
   * 将绑定滚动事件的元素以及相应的滚动事件保存在el的属性中,用于后续移除绑定事件
   */
  el.bindScrollElement = element;
  el.bindScrollListener = scrollEvent.bind(this, element, throttleCb, vnode);
  element.addEventListener('scroll', el.bindScrollListener);
}

export default {
  /**
   * 初始化设置,指令绑定到元素时调用
   * @param {*} el 指令绑定的元素
   * @param {*} binding 
   * @param {*} vnode 虚拟dom
   */
  bind(el, binding, vnode) {
    // 当前元素可能无滚动属性,需要向上遍历滚动元素,并且需要要在mounted事件触发之后,绑定滚动事件
    const vm = vnode.context;
    if (vm._isMounted) {
      bindEvent(el, binding, vnode);
    }
    vm.$on('hook:mounted', bindEvent.bind(this, el, binding, vnode))
  },

  unbind(el) {
    el.bindScrollElement.removeEventListener('scroll', el.bindScrollListener);
  }
}


// 在main.js中定义自定义指令
import InfiniteScroll from './InfiniteScroll/src/InfiniteScroll';
Vue.directive('InfiniteScroll', InfiniteScroll);


// 使用
<template>
  <div class="infinite-scroll-example" v-infinite-scroll="toLoad" distance="20" :loading="loading">
    <div class="item" v-for="(item, index) in count" :key="index">{{index}}</div>
    <div class="loading" v-if="loading">加载中</div>
    <div class="loaded" v-if="count >= 150">已经到底了</div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      loading: false,
      count: 100
    };
  },
  methods: {
    toLoad() {
      if (this.count >= 150) {
        return;
      }
      this.loading = true;
      setTimeout(() => {
        this.count += 10;
        this.loading = false;
      }, 2000);
    }
  }
};
</script>

<style lang="less" scoped>
.infinite-scroll-example {
  overflow: scroll;
  height: 100%;
  .item {
    font-size: 0.4rem;
    text-align: center;
  }
  .loading {
    font-size: 0.4rem;
    text-align: center;
  }
  .loaded {
    font-size: 0.4rem;
    text-align: center;
  }
}
</style>