Loadmore使用的时候分为下拉刷新和底部加载两种方式。css
下拉刷新的时候这样调用:html
<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>
底部加载的时候这样调用:node
<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组件:babel
<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的状态回归到默认状态。app