Vue中滚动加载更多的实现
louzhedong opened this issue · 0 comments
louzhedong commented
滚动加载是前端一个非常常见的场景,本文来实现一个基于Vue的滚动加载的功能
知识点:
- 加载更多主要是通过对滚动元素滚动距离的判断,来决定加载的时机
- Vue.directive 可以给 DOM 元素增加自定义指令,更方便地将滚动事件监听到对应的元素上
实现
-
获取滚动元素容器的高度
function getClientHeight(element) { return element.clientHeight; }
-
获取滚动元素滚动范围的高度
function getScrollHeight(element) { return element.scrollHeight; }
-
获取滚动元素已经滚动的高度
function getScrollTop(element) { return element.scrollTop; }
-
决定是否执行监听函数的判断
假设我们决定当页面滚动到里底部20距离的时候出发我们的监听函数,可以这样进行判断
if (scrollHeight - scrollTop - clientHeight < 20) { callback(); }
-
Vue 自定义指令的功能
-
bind
:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。 -
inserted
:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。 -
update
:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新 -
componentUpdated
:指令所在组件的 VNode 及其子 VNode 全部更新后调用。 -
unbind
:只调用一次,指令与元素解绑时调用。
在本例中,我们使用bind和unbind钩子函数
-
-
滚动事件是一个出发频率非常高的方法,需要使用节流函数来控制其频率
代码
// 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>