Loadmore
Loadmore使用的时候分为下拉刷新和底部加载两种方式。
下拉刷新的时候这样调用:
<template>
<div class="page-loadmore">
<h1 class="page-title">Pull down</h1>
<p class="page-loadmore-desc">在列表顶端, 按住 - 下拉 - 释放可以获取更多数据</p>
<p class="page-loadmore-desc">此例请使用手机查看</p>
<p class="page-loadmore-desc">translate : {{ translate }}</p>
<div class="loading-background" :style="{ transform: 'scale3d(' + moveTranslate + ',' + moveTranslate + ',1)' }">
translateScale : {{ moveTranslate }}
</div>
<div class="page-loadmore-wrapper" ref="wrapper" :style="{ height: wrapperHeight + 'px' }">
<!-- page-loadmore-wrapper元素是loadmore模块的父级盒子,它的高度是绑定了一个响应式的值wrapperHeight -->
<!-- 在生命周期mounted的时候为page-loadmore-wrapper计算高度 -->
<!-- page-loadmore-wrapper有一个ref属性,这就是给这个DOM元素添加了一个引用,在当前组件里可以用this.$refs的形式来调用这个DOM元素 -->
<loadmore :top-method="loadTop" @translate-change="translateChange" @top-status-change="handleTopChange" ref="loadmore">
<!-- loadmore组件,传进去了一个属性,loadTop会从props接收到 -->
<!-- loadTop方法用于给列表添加数据项 -->
<!-- 还给loadmore组件绑定了自定义事件top-status-change,用于更改topStatus这个属性值 -->
<!-- top-status-change的触发是在loadmore组件内部判断触发的,子组件$emit触发 -->
<ul class="page-loadmore-list">
<li v-for="(item, key, index) in list" :key="index" class="page-loadmore-listitem">{{ item }}</li>
</ul>
<!-- page-loadmore-list是数据列表 -->
<div slot="top" class="mint-loadmore-top">
<span v-show="topStatus !== 'loading'" :class="{ 'is-rotate': topStatus === 'drop' }">↓</span>
<span v-show="topStatus === 'loading'">
<a>加载中...</a>
</span>
</div>
<!-- top插槽插入的内容是下拉的时候,数据列表下移后上面出现的箭头和loading文字或者动画 -->
<!-- 箭头和文字都随着topStatus值来改变显示状态和样式 -->
<!-- topStatus有三种状态:pull,drop,loading -->
<!-- loading的时候显示文字或者动画,其它时候显示箭头 -->
</loadmore>
</div>
</div>
</template>
<style lang="scss" scoped>
.page-loadmore {
width: 100%;
overflow-x: hidden;
.page-loadmore-wrapper {
margin-top: -1px;
overflow: scroll;
.page-loadmore-listitem {
height: 50px;
line-height: 50px;
border-bottom: solid 1px #eee;
text-align: center;
&:first-child {
border-top: solid 1px #eee;
}
}
}
.loading-background {
width: 100%;
height: 50px;
line-height: 50px;
text-align: center;
transition: .2s linear;
}
.mint-loadmore-top {
span {
display: inline-block;
transition: .2s linear;
vertical-align: middle;
}
.is-rotate {
transform: rotate(180deg);
}
}
}
</style>
<script type="text/babel">
import loadmore from '@/components/loadmore'
export default {
data() {
return {
list: [],//数据列表
topStatus: '',//上方loading层状态
wrapperHeight: 0,//包裹盒子高度
translate: 0,
moveTranslate: 0
};
},
methods: {
handleTopChange(status) {//改变topStatus状态,下方的箭头和加载文字会随着topStatus改变样式或者内容
this.moveTranslate = 1;
this.topStatus = status;
},
translateChange(translate) {//loadmore组件在滑动时会触发此事件运行此方法
const translateNum = +translate;
this.translate = translateNum.toFixed(2);
this.moveTranslate = (1 + translateNum / 70).toFixed(2);
},
loadTop() {//加载更多数据列表
setTimeout(() => {
let firstValue = this.list[0];
for (let i = 1; i <= 10; i++) {
this.list.unshift(firstValue - i);
}
this.$refs.loadmore.onTopLoaded();//加载完数据之后调用loadmore组件的onTopLoaded方法
}, 1500);
}
},
components: {
loadmore
},
created() {//created的时候先给数据列表里填入20条数据
for (let i = 1; i <= 20; i++) {
this.list.push(i);
}
},
mounted() {
this.wrapperHeight = document.documentElement.clientHeight - this.$refs.wrapper.getBoundingClientRect().top;
//计算page-loadmore-wrapper的高度
//html元素的clientHeight - page-loadmore-wrapper盒子距离页面顶部的高度
//Element.getBoundingClientRect()方法返回元素的大小及其相对于视口的位置
//也就是除了页面上面的内容之外,下面整个就是page-loadmore-wrapper盒子,wrapper盒子给一个死高度之后,多给一个overflow:scroll;的样式,这样内容就可以通过滑动看到了
}
};
</script>
底部加载的时候这样调用:
<template>
<div class="page-loadmore">
<h1 class="page-title">Pull up</h1>
<p class="page-loadmore-desc">在列表底部, 按住 - 上拉 - 释放可以获取更多数据</p>
<p class="page-loadmore-desc">此例请使用手机查看</p>
<div class="page-loadmore-wrapper" ref="wrapper" :style="{ height: wrapperHeight + 'px' }">
<!-- page-loadmore-wrapper元素是loadmore模块的父级盒子,它的高度是绑定了一个响应式的值wrapperHeight -->
<!-- 在生命周期mounted的时候为page-loadmore-wrapper计算高度 -->
<!-- page-loadmore-wrapper有一个ref属性,这就是给这个DOM元素添加了一个引用,在当前组件里可以用this.$refs的形式来调用这个DOM元素 -->
<loadmore :bottom-method="loadBottom" @bottom-status-change="handleBottomChange" :bottom-all-loaded="allLoaded" ref="loadmore">
<!-- loadmore组件,传进去了两个属性,loadmore会从props接收到,loadBottom方法和allLoaded属性 -->
<!-- loadBottom方法用于给列表添加数据项,allLoaded是个布尔值,判断是否数据已经全部加载完了 -->
<!-- 还给loadmore组件绑定了一个自定义事件bottom-status-change,用于更改bottomStatus这个属性值 -->
<!-- bottom-status-change的触发是在loadmore组件内部判断触发的,子组件$emit触发 -->
<ul class="page-loadmore-list">
<li v-for="(item, key, index) in list" :key="index" class="page-loadmore-listitem">{{ item }}</li>
</ul>
<!-- page-loadmore-list是数据列表 -->
<div slot="bottom" class="mint-loadmore-bottom">
<span v-show="bottomStatus !== 'loading'" :class="{ 'is-rotate': bottomStatus === 'drop' }">↑</span>
<span v-show="bottomStatus === 'loading'">
<a>加载中...</a>
</span>
</div>
<!-- bottom插槽插入的内容是上拉的时候,数据列表上移后下面出现的箭头和loading文字或者动画 -->
<!-- 箭头和文字都随着bottomStatus值来改变显示状态和样式 -->
<!-- bottomStatus有三种状态:pull,drop,loading -->
<!-- loading的时候显示文字或者动画,其它时候显示箭头 -->
</loadmore>
<!-- loadmore组件有三个插槽,top,bottom和默认插槽 -->
<!-- top是列表下移后上方出现的箭头和loading文字,bottom是上移后下方出现的箭头和文字,默认插槽就是数据列表 -->
</div>
</div>
</template>
<style lang="scss" scoped>
.page-loadmore-listitem {
height: 50px;
line-height: 50px;
border-bottom: solid 1px #eee;
text-align: center;
}
.page-loadmore-wrapper {
overflow: scroll;
}
.mint-loadmore-bottom {
span {
display: inline-block;
transition: .2s linear;
}
.is-rotate {
transform: rotate(180deg);
}
}
</style>
<script>
import loadmore from '@/components/loadmore'
export default {
data () {
return {
list: [],//数据列表
allLoaded: false,//是否全部加载
bottomStatus: '',//下方loading层状态
wrapperHeight: 0//包裹盒子高度
}
},
methods: {
handleBottomChange(status) {//改变bottomStatus状态,下方的箭头和加载文字会随着bottomStatus改变样式或者内容
this.bottomStatus = status;
},
loadBottom() {//加载更多数据列表
setTimeout(() => {
let lastValue = this.list[this.list.length - 1];
if (lastValue < 40) {
for (let i = 1; i <= 10; i++) {
this.list.push(lastValue + i);
}
} else {
this.allLoaded = true;//数据全部加载完了,就改变allLoaded
}
this.$refs.loadmore.onBottomLoaded();//加载完数据之后调用loadmore组件的onBottomLoaded方法
}, 1500);
}
},
components: {
loadmore
},
created () {//created的时候先给数据列表里填入20条数据
for (let i = 0; i <= 20; i++) {
this.list.push(i)
}
},
mounted () {
this.wrapperHeight = document.documentElement.clientHeight - this.$refs.wrapper.getBoundingClientRect().top
//计算page-loadmore-wrapper的高度
//html元素的clientHeight - page-loadmore-wrapper盒子距离页面顶部的高度
//Element.getBoundingClientRect()方法返回元素的大小及其相对于视口的位置
//也就是除了页面上面的内容之外,下面整个就是page-loadmore-wrapper盒子,wrapper盒子给一个死高度之后,多给一个overflow:scroll;的样式,这样内容就可以通过滑动看到了
}
}
</script>
loadmore组件:
<template>
<div class="mint-loadmore">
<!-- mint-loadmore最外层盒子,有overflow:hidden;的样式,这样下方的箭头文字动画或者上方的就会隐藏看不到 -->
<div class="mint-loadmore-content" :class="{ 'is-dropped': topDropped || bottomDropped}" :style="{ 'transform': transform }">
<!-- content盒子拥有两个响应式属性,一个在drop的时候添加is-dropped类名,让transform变化更流畅,一个是transform样式,在touchmove的时候,会改变盒子在垂直方向的位置 -->
<!-- transfrom样式的值是一个计算属性,会随着this.translate变化而变化 -->
<slot name="top">
<div class="mint-loadmore-top" v-if="topMethod">
<span v-if="topStatus === 'loading'" class="mint-loadmore-spinner"></span>
<span class="mint-loadmore-text">{{ topText }}</span>
</div>
<!-- top插槽,列表上拉后下方出现的箭头和loading文字或者动画 -->
<!-- 当有topMethod这个props传入的时候才显示,此处是备用内容,如果父组件定义了top插槽内容,则备用内容不显示 -->
</slot>
<slot></slot>
<!-- 默认插槽,就是数据列表 -->
<slot name="bottom">
<div class="mint-loadmore-bottom" v-if="bottomMethod">
<span v-if="bottomStatus === 'loading'" class="mint-loadmore-spinner"></span>
<span class="mint-loadmore-text">{{ bottomText }}</span>
</div>
</slot>
<!-- bottom插槽,列表上拉后下方出现的箭头和loading文字或者动画 -->
<!-- 当有bottomMethod这个props传入的时候才显示,此处是备用内容,如果父组件定义了bottom插槽内容,则备用内容不显示 -->
</div>
</div>
</template>
<style lang="scss" scoped>
.mint-loadmore {
overflow: hidden;
}
.mint-loadmore-content .is-dropped {
transition: .2s;
}
.mint-loadmore-bottom {
text-align: center;
height: 50px;
line-height: 50px;
margin-bottom: -50px;
}
.mint-loadmore-top {
text-align: center;
height: 50px;
line-height: 50px;
margin-top: -50px;
}
</style>
<script type="text/babel">
export default {
name: 'loadmore',
components: {
},
props: {//props后跟着的对象是验证器,type是类型,default是默认值
maxDistance: {
type: Number,
default: 0
},
autoFill: {
type: Boolean,
default: true
},
distanceIndex: {
type: Number,
default: 2
},
topPullText: {
type: String,
default: '下拉刷新'
},
topDropText: {
type: String,
default: '释放更新'
},
topLoadingText: {
type: String,
default: '加载中...'
},
topDistance: {
type: Number,
default: 70
},
topMethod: {
type: Function
},
bottomPullText: {
type: String,
default: '上拉刷新'
},
bottomDropText: {
type: String,
default: '释放更新'
},
bottomLoadingText: {
type: String,
default: '加载中...'
},
bottomDistance: {
type: Number,
default: 70
},
bottomMethod: {//加载下方数据方法
type: Function
},
bottomAllLoaded: {//布尔值,下方数据已经全部加载
type: Boolean,
default: false
}
},
data() {
return {
translate: 0, //content在y轴移动距离
scrollEventTarget: null, //scroll元素
containerFilled: false, //当前滚动的内容是否填充完整
topText: '', //上方提示文字
topDropped: false, //下拉刷新是否已经释放
bottomText: '', //下方提示文字
bottomDropped: false, //底部加载是否已经释放
bottomReached: false, //是否已经到达底部
direction: '', //滑动方向
startY: 0, //开始滑动的时候触点的的Y坐标
startScrollTop: 0, //开始滑动的时候,scroll盒子的滚动距离
currentY: 0, //move过程中触点的y轴坐标
topStatus: '', //上方loading层状态,更新后会传给父组件
bottomStatus: '' //下方loading层状态,更新后会传给父组件
};
},
computed: {
transform() {//计算属性transform,,根据translate值变化,用于通过transform样式改变content盒子的y轴坐标
return this.translate === 0 ? null : 'translate3d(0, ' + this.translate + 'px, 0)';
}
},
watch: {
topStatus(val) {
//侦听器,如果topStatus发生变化,这个函数就会运行,触发父级组件的事件,并把topStatus新值作为参数传过去
this.$emit('top-status-change', val);
switch (val) {
case 'pull':
this.topText = this.topPullText;
break;
case 'drop':
this.topText = this.topDropText;
break;
case 'loading':
this.topText = this.topLoadingText;
break;
}
//根据topStatus的新值改变上方的提示文字
},
bottomStatus(val) {
//侦听器,如果bottomStatus发生变化,这个函数就会运行,触发父级组件的事件,并把bottomStatus新值作为参数传过去
this.$emit('bottom-status-change', val);
switch (val) {
case 'pull':
this.bottomText = this.bottomPullText;
break;
case 'drop':
this.bottomText = this.bottomDropText;
break;
case 'loading':
this.bottomText = this.bottomLoadingText;
break;
}
//根据bottomStatus的新值改变下方的提示文字
}
},
methods: {
onTopLoaded() {//父级组件里每次加载完新数据就会调用这个方法
this.translate = 0;//重置this.translate
setTimeout(() => {
this.topStatus = 'pull';//数据加载完之后topStatus变为pull状态
}, 200);
},
onBottomLoaded() {//父级组件里每次加载完新数据就会调用这个方法
this.bottomStatus = 'pull'; //数据加载完之后bottomStatus变为pull状态
this.bottomDropped = false; //数据加载完之后bottomDropped变为false
this.$nextTick(() => {//数据变化后会更新DOM,DOM更新后会调用$nextTick()里的方法
if (this.scrollEventTarget === window) {
document.body.scrollTop += 50;
} else {
this.scrollEventTarget.scrollTop += 50;
}//数据加载完之后让对应的scroll盒子向下多滚动50px,也就是说多显示一条数据让用户看到
this.translate = 0;//重置this.translate
});
if (!this.bottomAllLoaded && !this.containerFilled) {
this.fillContainer();
}
},
getScrollEventTarget(element) {//获取overflow:scroll的父级盒子
let currentNode = element;
while (currentNode && currentNode.tagName !== 'HTML' &&
currentNode.tagName !== 'BODY' && currentNode.nodeType === 1) {
//当前传入节点存在且不是html也不是body且是一个元素节点的时候
let overflowY = document.defaultView.getComputedStyle(currentNode).overflowY;
//document.defaultView返回document关联的window对象
//getComputedStyle()获取元素的计算样式
//overflowY是当前传入节点的计算样式overflow-y
if (overflowY === 'scroll' || overflowY === 'auto') {
return currentNode; //如果当前节点的overflow-y值是scroll或者auto,那就返回此节点
}
currentNode = currentNode.parentNode;//如果不是,那就获取当前节点的父节点,然后继续判断
}
return window;//如果都找不到就返回window对象
},
getScrollTop(element) {//获取元素的内容滚动距离
if (element === window) {
return Math.max(window.pageYOffset || 0, document.documentElement.scrollTop);
//window.pageYOffset就是Window.scrollY,文档在垂直方向滚动距离
} else {
return element.scrollTop;
}
},
bindTouchEvents() {//为mint-loadmore绑定touch事件操作
this.$el.addEventListener('touchstart', this.handleTouchStart);
this.$el.addEventListener('touchmove', this.handleTouchMove);
this.$el.addEventListener('touchend', this.handleTouchEnd);
},
init() {
this.topStatus = 'pull';//topStatus初始值为pull
this.bottomStatus = 'pull';//bottomStatus初始值为pull
this.topText = this.topPullText;
this.scrollEventTarget = this.getScrollEventTarget(this.$el);
//获取overflow:scroll的父级盒子
//传给getScrollEventTarget方法的参数是this.$el,它是当前Vue实例使用的根DOM元素,也就是mint-loadmore
//this.scrollEventTarget最后获取到是父组件的page-loadmore-wrapper盒子,因为它overflow:scroll;
if (typeof this.bottomMethod === 'function') {//父级组件传入的加载数据函数如果存在的话
this.fillContainer();//判断是否数据填充完全,初始化this.containerFilled的值
this.bindTouchEvents();//为mint-loadmore绑定touch事件操作
}
if (typeof this.topMethod === 'function') {//父级组件传入的加载数据函数如果存在的话
this.bindTouchEvents();//为mint-loadmore绑定touch事件操作
}
},
fillContainer() {//判断是否数据填充完全
if (this.autoFill) {
this.$nextTick(() => {
if (this.scrollEventTarget === window) {
this.containerFilled = this.$el.getBoundingClientRect().bottom >=
document.documentElement.getBoundingClientRect().bottom;
} else {
this.containerFilled = this.$el.getBoundingClientRect().bottom >=
this.scrollEventTarget.getBoundingClientRect().bottom;
//如果mint-loadmore的bottom值大于等于滚动盒子的bottom值,说明数据填充完全了,this.containerFilled为true
}
if (!this.containerFilled) {
this.bottomStatus = 'loading';
this.bottomMethod();
//如果数据并没有填充完全,则bottomStatus状态为loading,执行父组件的加载数据方法
}
});
}
},
checkBottomReached() {//检查是否已经滑到底部
if (this.scrollEventTarget === window) {
/**
* fix:scrollTop===0
*/
return document.documentElement.scrollTop || document.body.scrollTop + document.documentElement.clientHeight >= document.body.scrollHeight;
//如果scroll元素是window的话,就判断文档滑动距离加上文档高度是否大于等于body的内容高度
} else {
return parseInt(this.$el.getBoundingClientRect().bottom) <= parseInt(this.scrollEventTarget.getBoundingClientRect().bottom) + 1;
}
},
handleTouchStart(event) {
this.startY = event.touches[0].clientY;
//TouchEvent.touches返回所有当前在与触摸表面接触的Touch对象
//Touch对象表示在触控设备上的触摸点
//Touch.clientY,触点相对于可见视区上边沿的的Y坐标
//this.startY是开始滑动的时候触点的Y坐标
this.startScrollTop = this.getScrollTop(this.scrollEventTarget);
//开始滑动的时候,scroll盒子的滚动距离
this.bottomReached = false;
//是否已经滑动到底部
if (this.topStatus !== 'loading') {//如果上方提示块并未处于加载阶段就重置topStatus和topDropped
this.topStatus = 'pull';
this.topDropped = false;
}
if (this.bottomStatus !== 'loading') {//如果下方提示块并未处于加载阶段就重置bottomStatus和bottomDropped
this.bottomStatus = 'pull';
this.bottomDropped = false;
}
},
handleTouchMove(event) {
if (this.startY < this.$el.getBoundingClientRect().top && this.startY > this.$el.getBoundingClientRect().bottom) {
return;
}
//如果触点在mint-loadmore之外就退出move事件
this.currentY = event.touches[0].clientY;
//this.currentY是move过程中触点的y轴坐标
let distance = (this.currentY - this.startY) / this.distanceIndex;
//滑动的距离
this.direction = distance > 0 ? 'down' : 'up';
//判断滑动方向
if (typeof this.topMethod === 'function' && this.direction === 'down' &&
this.getScrollTop(this.scrollEventTarget) === 0 && this.topStatus !== 'loading') {
//如果滑到了顶部
event.preventDefault();//阻止默认事件
event.stopPropagation();//阻止事件冒泡
if (this.maxDistance > 0) {
this.translate = distance <= this.maxDistance ? distance - this.startScrollTop : this.translate;
} else {
this.translate = distance - this.startScrollTop;
//随着滑动来更新translate值,translate值变化,计算属性transform就随之变化,content盒子就在y轴上向下移动
}
if (this.translate < 0) {//刚滑到顶部滑不动,会顿一下
this.translate = 0;
}
this.topStatus = this.translate >= this.topDistance ? 'drop' : 'pull';
//topDistance默认70,拉动距离超过70下方箭头就变个方向
}
if (this.direction === 'up') {//如果是向上滑动,那就是底部加载,就判断是否已经滑到底部
this.bottomReached = this.bottomReached || this.checkBottomReached();
}
if (typeof this.bottomMethod === 'function' && this.direction === 'up' &&
this.bottomReached && this.bottomStatus !== 'loading' && !this.bottomAllLoaded) {
//如果拉到底部了且数据没有加载完
event.preventDefault();//阻止默认事件
event.stopPropagation();//阻止事件冒泡
if (this.maxDistance > 0) {
this.translate = Math.abs(distance) <= this.maxDistance
? this.getScrollTop(this.scrollEventTarget) - this.startScrollTop + distance : this.translate;
} else {
this.translate = this.getScrollTop(this.scrollEventTarget) - this.startScrollTop + distance;
//随着滑动来更新translate值,translate值变化,计算属性transform就随之变化,content盒子就在y轴上向上移动
}
if (this.translate > 0) {//刚滑到底部滑不动,会顿一下
this.translate = 0;
}
this.bottomStatus = -this.translate >= this.bottomDistance ? 'drop' : 'pull';
//bottomDistance默认70,拉动距离超过70下方箭头就变个方向
}
this.$emit('translate-change', this.translate);//触发父组件事件,这个是上拉刷新的时候用的
},
handleTouchEnd() {
if (this.direction === 'down' && this.getScrollTop(this.scrollEventTarget) === 0 && this.translate > 0) {
//下拉刷新
this.topDropped = true;//drop状态变更,content添加is-dropped样式,回到原点动画
if (this.topStatus === 'drop') {
this.translate = '50';
this.topStatus = 'loading';
this.topMethod();
//如果topStatus还是drop状态,说明刚放手,那就让content回到距离顶部50px的地方,然后改变topStatus为loading,然后执行父组件加载新数据的方法
} else {
this.translate = '0';
this.topStatus = 'pull';
//如果没有从超过70的地方释放,那就回到初始状态,不加载新数据
}
}
if (this.direction === 'up' && this.bottomReached && this.translate < 0) {
//底部加载
this.bottomDropped = true;//drop状态变更,content添加is-dropped样式,回到原点动画
this.bottomReached = false;//改变是否到达底部状态
if (this.bottomStatus === 'drop') {
this.translate = '-50';
this.bottomStatus = 'loading';
this.bottomMethod();
//如果bottomStatus还是drop状态,说明刚放手,那就让content回到距离底部50px的地方,然后改变bottomStatus为loading,然后执行父组件加载新数据的方法
} else {
this.translate = '0';
this.bottomStatus = 'pull';
//如果没有从超过70的地方释放,那就回到初始状态,不加载新数据
}
}
this.$emit('translate-change', this.translate);//触发父组件事件,这个是上拉刷新的时候用的
this.direction = '';//清空方向
}
},
mounted() {//mounted的时候调用init()初始化组件状态
this.init();
}
};
</script>
实现原理就是,外面有个wrapper盒子有死高度且拥有样式overflow:scroll;的样式,这样它的内容超出后就是可滚动的,它的滚动高度scrollTop就可以拿来计算用。wrapper盒子里的内容除了数据列表以外还有一个loading层,这个loading层就是已经到顶部再继续下拉 或者 已经到底部再上拉的时候才会显示出来,平时的时候利用maring负值改变y轴方向的位置隐藏起来,它里面就是loading动画和一个小箭头提示标志,滑动的时候改变里面content盒子的y轴坐标,然后将loading层显示出来,释放的时候让content盒子回到距离原位一个loading层高度的地方,然后发请求加载数据,等数据加载好了再次把所有DOM的状态回归到默认状态。

浙公网安备 33010602011771号