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