详解vue mint-ui源码解析之loadmore组件
本文介绍了vue mint-ui源码解析之loadmore组件,分享给大家,具体如下:
接入
官方接入文档
接入使用example
html
<div id="app"> <mt-loadmore :top-method="loadtop" :bottom-method="loadbottom" :bottom-all-loaded="allloaded" :max-distance="150" @top-status-change="handletopchange" ref="loadmore"> <div slot="top" class="mint-loadmore-top"> <span v-show="topstatus === 'pull'" :class="{ 'rotate': topstatus === 'drop' }">↓</span> <span v-show="topstatus === 'loading'">loading...</span> <span v-show="topstatus === 'drop'">释放更新</span> </div> <ul class="scroll-wrapper"> <li v-for="item in list" @click="itemclick(item)">{{ item }}</li> </ul> </mt-loadmore> </div>
css
<link rel="stylesheet" href="https://unpkg.com/mint-ui/lib/style.css" rel="external nofollow" > *{ margin: 0; padding: 0; } html, body{ height: 100%; } #app{ height: 100%; overflow: scroll; } .scroll-wrapper{ margin: 0; padding: 0; list-style: none; } .scroll-wrapper li{ line-height: 120px; font-size: 60px; text-align: center; }
js
<!-- 先引入 vue --> <script src="https://unpkg.com/vue/dist/vue.js"></script> <!-- 引入组件库 --> <script src="https://unpkg.com/mint-ui/lib/index.js"></script> <script> new vue({ el: '#app', data: { list: [], allloaded: false, topstatus: '' }, created: function () { var i =0, len=20; for (; i< len; i++){ this.list.push('demo' + i); } }, methods: { loadtop: function () { // 刷新数据的操作 var self = this; settimeout(function () { self.list.splice(0, self.list.length); var i =0, len=20; for (; i< len; i++){ self.list.push('demo' + i); } self.$refs.loadmore.ontoploaded(); }, 2000); }, loadbottom: function () { // 加载更多数据的操作 //load data //this.allloaded = true;// 若数据已全部获取完毕 var self = this; settimeout(function () { var i =0; len = 10; for (; i< len; i++){ self.list.push('dddd' + i); } self.$refs.loadmore.onbottomloaded(); }, 2000); }, handletopchange: function (status) { this.topstatus = status; }, itemclick: function (data) { console.log('item click, msg : ' + data); } } }); </script>
实现原理解析
布局原理
- loadmore组件内部由三个slot组成,分别为name=top,name=bottom,default;
- top用于展示下拉刷新不同状态展示的内容,初始设置margin-top为-top的高度来将自己隐藏
- bottom同top,用于展示上拉加载更多不同状态展示的内容
- default填充滚动详细内容
实现原理
- 主要是通过js的touch事件的监听来实现
- 在touchmove事件,如果是向下滑动并且滚动的dom的scrolltop为0,那么整个组件向下偏移(滑动的距离/比值)展示出top solt的内容
- 在touchmove时间,如果是向上滑动并且滑动到了底部,再继续滑动整个组件向上偏移(滑动的距离/比值)展示出bottom solt的内容
源码解析
组件的template html
<div class="mint-loadmore"> <div class="mint-loadmore-content" :class="{ 'is-dropped': topdropped || bottomdropped}" :style="{ 'transform': 'translate3d(0, ' + translate + 'px, 0)' }"> <slot name="top"> <div class="mint-loadmore-top" v-if="topmethod"> <spinner v-if="topstatus === 'loading'" class="mint-loadmore-spinner" :size="20" type="fading-circle"></spinner> <span class="mint-loadmore-text">{{ toptext }}</span> </div> </slot> <slot></slot> <slot name="bottom"> <div class="mint-loadmore-bottom" v-if="bottommethod"> <spinner v-if="bottomstatus === 'loading'" class="mint-loadmore-spinner" :size="20" type="fading-circle"></spinner> <span class="mint-loadmore-text">{{ bottomtext }}</span> </div> </slot> </div> </div>
关于 上面的spinner标签,是一个组件,这里不做详细介绍。top solt和bottom slot中的内容是展示的内容,可以通过外部自定义的方式传入。
其实它的实现有一个很严重的弊端,就是写死了top solt和bottom slot的高度为50px,而且js中的处理也是使用50px进行的逻辑处理。所以并满足我们开发中自定义top slot 和bottom slot的需求。
js核心解析
- props解析:关于props的解析,可以参考mint-ui的官方文档
- data解析
data() { return { translate: 0, // 此变量决定当前组件上下移动, scrolleventtarget: null, // 滚动的dom节点 containerfilled: false, // 当前滚动的内容是否填充完整,不完成会调用 loadmore的回调函数 toptext: '', // 下拉刷新,显示的文本 topdropped: false, // 记录当前drop状态,用给组件dom添加is-dropped class(添加回到原点的动画) bottomtext: '', // 上拉加载更多 显示的文本 bottomdropped: false, // 同topdropped bottomreached: false, // 当前滚动是否滚动到了底部 direction: '', // touch-move过程中, 当前滑动的方向 starty: 0, // touch-start 起始的y的坐标值 startscrolltop: 0, // touch-start 起始的滚动dom的 scrolltop currenty: 0, // touch-move 过程中的 y的坐标 topstatus: '', // 下拉刷新的状态: pull(下拉) drop(释放) loading(正在加载数据) bottomstatus: '' // 上拉加载更多的状态: 状态同上 }; }
上面的关于每个data数据的具体作用通过注释做了详细说明。
watch解析
watch: { topstatus(val) { 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; } }, bottomstatus(val) { 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; } } }
上面是组件通过watch监听的两个变量,后面我们能看到他们的改变是在touchmove事件中进行处理改变的。它的作用是通过它的变化来改变top slot和bottom slot的文本内容;
同时发出status-change事件给外部使用,因为可能外部自定义top slot 和bottom slot的内容,通过此事件来通知外部当前状态以便外部进行处理。
核心函数的解析
这里就不将所有的method列出,下面就根据处理的所以来解析对应的method函数。
首先,入口是在组件mounted生命周期的钩子回调中执行init函数
mounted() { this.init();// 当前 vue component挂载完成之后, 执行init()函数 }
init函数:
init() { this.topstatus = 'pull'; this.bottomstatus = 'pull'; this.toptext = this.toppulltext; this.scrolleventtarget = this.getscrolleventtarget(this.$el); // 获取滚动的dom节点 if (typeof this.bottommethod === 'function') { this.fillcontainer(); // 判断当前滚动内容是否填满,没有执行外部传入的loadmore回调函数加载数据 this.bindtouchevents(); // 为当前组件dom注册touch事件 } if (typeof this.topmethod === 'function') { this.bindtouchevents(); } }, 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; } if (!this.containerfilled) { // 如果没有填满内容, 执行loadmore的操作 this.bottomstatus = 'loading'; this.bottommethod();// 调用外部的loadmore函数,加载更多数据 } }); } }
init函数主要是初始化状态和事件的一些操作,下面着重分析touch事件的回调函数的处理。
首先touchstart事件回调处理函数
handletouchstart(event) { this.starty = event.touches[0].clienty; // 手指按下的位置, 用于下面move事件计算手指移动的距离 this.startscrolltop = this.getscrolltop(this.scrolleventtarget); // 起始scroll dom的 scrolltop(滚动的距离) //下面重置状态变量 this.bottomreached = false; if (this.topstatus !== 'loading') { this.topstatus = 'pull'; this.topdropped = false; } if (this.bottomstatus !== 'loading') { this.bottomstatus = 'pull'; this.bottomdropped = false; } }
主要是记录初始位置和重置状态变量。
下面继续touchmove的回调处理函数
handletouchmove(event) { //确保当前touch节点的y的位置,在当前loadmore组件的内部 if (this.starty < this.$el.getboundingclientrect().top && this.starty > this.$el.getboundingclientrect().bottom) { return; } this.currenty = event.touches[0].clienty; let distance = (this.currenty - this.starty) / this.distanceindex; this.direction = distance > 0 ? 'down' : 'up'; // 下拉刷新,条件(1.外部传入了刷新的回调函数 2.滑动方向是向下的 3.当前滚动节点的scrolltop为0 4.当前topstatus不是loading) if (typeof this.topmethod === 'function' && this.direction === 'down' && this.getscrolltop(this.scrolleventtarget) === 0 && this.topstatus !== 'loading') { event.preventdefault(); event.stoppropagation(); //计算translate(将要平移的距离), 如果当前移动的距离大于设置的最大距离,那么此次这次移动就不起作用了 if (this.maxdistance > 0) { this.translate = distance <= this.maxdistance ? distance - this.startscrolltop : this.translate; } else { this.translate = distance - this.startscrolltop; } if (this.translate < 0) { this.translate = 0; } this.topstatus = this.translate >= this.topdistance ? 'drop' : 'pull';// drop: 到达指定的阈值,可以执行刷新操作了 } // 上拉操作, 判断当前scroll dom是否滚动到了底部 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; } if (this.translate > 0) { this.translate = 0; } this.bottomstatus = -this.translate >= this.bottomdistance ? 'drop' : 'pull'; } this.$emit('translate-change', this.translate); }
上面的代码逻辑挺简单,注释也就相对不多。
重点谈一下checkbottomreached()函数,用来判断当前scroll dom是否滚动到了底部。
checkbottomreached() { if (this.scrolleventtarget === window) { return document.body.scrolltop + document.documentelement.clientheight >= document.body.scrollheight; } else { return this.$el.getboundingclientrect().bottom <= this.scrolleventtarget.getboundingclientrect().bottom + 1; } }
经过我的测试,上面的代码是有问题:
当scrolleventtarget是window的条件下,上面的判断是不对的。因为document.body.scrolltop总是比正常值小1,所以用于无法满足到达底部的条件;
当scrolleventtarget不是window的条件下,上面的判断条件也不需要在this.scrolleventtarget.getboundingclientrect().bottom后面加1,但是加1也不会有太大视觉上的影响。
最后来看下moveend事件回调的处理函数
handletouchend() { if (this.direction === 'down' && this.getscrolltop(this.scrolleventtarget) === 0 && this.translate > 0) { this.topdropped = true; // 为当前组件添加 is-dropped class(也就是添加动画处理) if (this.topstatus === 'drop') { // 到达了loading的状态 this.translate = '50'; // top slot的高度 this.topstatus = 'loading'; this.topmethod(); // 执行回调函数 } else { // 没有到达,回调原点 this.translate = '0'; this.topstatus = 'pull'; } } // 处理逻辑同上 if (this.direction === 'up' && this.bottomreached && this.translate < 0) { this.bottomdropped = true; this.bottomreached = false; if (this.bottomstatus === 'drop') { this.translate = '-50'; this.bottomstatus = 'loading'; this.bottommethod(); } else { this.translate = '0'; this.bottomstatus = 'pull'; } } this.$emit('translate-change', this.translate); this.direction = ''; } }
总结
- 下拉刷新和上拉加载更多的实现原理可以借鉴
- getscrolleventtarget()获取滚动对象,getscrolltop()获取滚动距离,checkbottomreached()判断是否滚动到底部;这三种方式可以借鉴
- 缺点: 写死了top slot 和 bottom slot的高度,太不灵活;这个地方可以优化
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。