您好,登錄后才能下訂單哦!
小編給大家分享一下無(wú)限滾動(dòng)插件vue-infinite-scroll的示例分析,相信大部分人都還不怎么了解,因此分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后大有收獲,下面讓我們一起去了解一下吧!
插件使用方法
這是一個(gè) vue 的指令,按照 github 倉(cāng)庫(kù)上的介紹,用法挺簡(jiǎn)單的,例如:
<div class="app" v-infinite-scroll="loadMore" infinite-scroll-disabled="busy" infinite-scroll-distance="10"> <div class="content"></div> <div class="loading" v-show="busy">loading.....</div> </div>
.app { height: 1000px; border: 1px solid red; width: 600px; margin: 0 auto; overflow: auto; } .content { height: 1300px; background-color: #ccc; width: 80%; margin: 0 auto; } .loading { font-weight: bold; font-size: 20px; color: red; text-align: center; }
var app = document.querySelector('.app'); new Vue({ el: app, directives: { InfiniteScroll, }, data: function() { return { busy: false }; }, methods: { loadMore: function() { var self = this; self.busy = true; console.log('loading... ' + new Date()); setTimeout(function() { var target = document.querySelector('.content'); var height = target.clientHeight; target.style.height = height + 300 + 'px'; console.log('end... ' + new Date()); self.busy = false; }, 1000); }, }, });
這里的指令宿主元素自身設(shè)置了 overflow:auto
,內(nèi)部元素用來(lái)支撐滾動(dòng),當(dāng)滾動(dòng)到底部時(shí),增加內(nèi)部元素的高度從而模擬了無(wú)限滾動(dòng)。效果如下:
另外可以將父元素設(shè)置為滾動(dòng),當(dāng)自身滾動(dòng)到父元素底部時(shí),增加自身的高度,模擬拉取下一頁(yè)數(shù)據(jù)的操作。 例如:
<div class="app"> <div class="content" v-infinite-scroll="loadMore" infinite-scroll-disabled="busy" infinite-scroll-distance="10"></div> <div class="loading" v-show="busy">loading.....</div> </div>
達(dá)到的效果和上面完全相同。
源碼解析
接下來(lái)就是看看內(nèi)部怎么實(shí)現(xiàn)的。照例從入口開始看起。因?yàn)檫@個(gè)插件就是一個(gè) vue
的指令,所以入口還是挺簡(jiǎn)單的:
指令入口
export default { bind(el, binding, vnode) { el[ctx] = { el, vm: vnode.context, expression: binding.value, // 滾動(dòng)到底部時(shí)需要的監(jiān)聽函數(shù),通常用于加載下一頁(yè)數(shù)據(jù) }; const args = arguments; // 監(jiān)聽宿主元素所在組件的mounted事件 el[ctx].vm.$on('hook:mounted', function() { el[ctx].vm.$nextTick(function() { // 判斷元素是否已經(jīng)在頁(yè)面上 if (isAttached(el)) { // 獲取各項(xiàng)指令相關(guān)屬性,執(zhí)行各種事件綁定 doBind.call(el[ctx], args); } el[ctx].bindTryCount = 0; // 間隔50ms輪訓(xùn)10次,判斷元素是否已經(jīng)在頁(yè)面上 var tryBind = function() { if (el[ctx].bindTryCount > 10) return; //eslint-disable-line el[ctx].bindTryCount++; if (isAttached(el)) { doBind.call(el[ctx], args); } else { setTimeout(tryBind, 50); } }; tryBind(); }); }); }, unbind(el) { // 事件解綁 if (el && el[ctx] && el[ctx].scrollEventTarget) el[ctx].scrollEventTarget.removeEventListener('scroll', el[ctx].scrollListener); }, };
核心就是在宿主元素渲染后,執(zhí)行 doBind
方法,我們猜測(cè)會(huì)在 doBind
綁定滾動(dòng)父元素的 scroll
事件。
isAttached
方法用于判斷一個(gè)元素是否已渲染在頁(yè)面上,判斷方法是查看是否有組件元素的標(biāo)簽名為 HTML
:
// 判斷元素是否已經(jīng)在頁(yè)面上 var isAttached = function(element) { var currentNode = element.parentNode; while (currentNode) { if (currentNode.tagName === 'HTML') { return true; } // 11 表示DomFragment if (currentNode.nodeType === 11) { return false; } currentNode = currentNode.parentNode; } return false; };
參數(shù)解析與事件綁定
現(xiàn)在看看 doBind
方法,邏輯比較多,不過(guò)都不難。
var doBind = function() { if (this.binded) return; // 只綁定一次 this.binded = true; var directive = this; var element = directive.el; // throttleDelayExpr: 截流間隔。 設(shè)置在元素的屬性上 var throttleDelayExpr = element.getAttribute('infinite-scroll-throttle-delay'); var throttleDelay = 200; if (throttleDelayExpr) { // 優(yōu)先嘗試組件上的throttleDelayExpr屬性值, 如 <div infinite-scroll-throttle-delay="myDelay"></div> throttleDelay = Number(directive.vm[throttleDelayExpr] || throttleDelayExpr); if (isNaN(throttleDelay) || throttleDelay < 0) { throttleDelay = 200; } } directive.throttleDelay = throttleDelay; // 監(jiān)聽滾動(dòng)父元素的scroll時(shí)間,監(jiān)聽函數(shù)設(shè)置了函數(shù)截流 directive.scrollEventTarget = getScrollEventTarget(element); // 設(shè)置了滾動(dòng)的父元素 directive.scrollListener = throttle(doCheck.bind(directive), directive.throttleDelay); directive.scrollEventTarget.addEventListener('scroll', directive.scrollListener); this.vm.$on('hook:beforeDestroy', function() { directive.scrollEventTarget.removeEventListener('scroll', directive.scrollListener); }); // infinite-scroll-disabled: 是否禁用無(wú)限滾動(dòng) // 可以為表達(dá)式 var disabledExpr = element.getAttribute('infinite-scroll-disabled'); var disabled = false; if (disabledExpr) { this.vm.$watch(disabledExpr, function(value) { directive.disabled = value; // 當(dāng)disable為false時(shí),重啟check if (!value && directive.immediateCheck) { doCheck.call(directive); } }); disabled = Boolean(directive.vm[disabledExpr]); } directive.disabled = disabled; // 宿主元素到滾動(dòng)父元素底部的距離閾值,小于這個(gè)值時(shí),觸發(fā)listen-for-event監(jiān)聽函數(shù) var distanceExpr = element.getAttribute('infinite-scroll-distance'); var distance = 0; if (distanceExpr) { distance = Number(directive.vm[distanceExpr] || distanceExpr); if (isNaN(distance)) { distance = 0; } } directive.distance = distance; // immediate-check:是否在bind后立即檢查一遍,也會(huì)在disable失效時(shí)立即觸發(fā)檢查 var immediateCheckExpr = element.getAttribute('infinite-scroll-immediate-check'); var immediateCheck = true; if (immediateCheckExpr) { immediateCheck = Boolean(directive.vm[immediateCheckExpr]); } directive.immediateCheck = immediateCheck; if (immediateCheck) { doCheck.call(directive); } // 當(dāng)組件上設(shè)置的此事件觸發(fā)時(shí),執(zhí)行一次檢查 var eventName = element.getAttribute('infinite-scroll-listen-for-event'); if (eventName) { directive.vm.$on(eventName, function() { doCheck.call(directive); }); } };
整個(gè)看下來(lái),核心就是利用各種參數(shù)控制 doCheck
的調(diào)用,包括時(shí)間間隔、 disabled
、距離閾值、 immediate-check
、組件事件。
doCheck
因?yàn)闀?huì)非常頻繁的調(diào)用,所以用 throttle
進(jìn)行了截流,具體邏輯這里不再贅述。
在 getScrollEventTarget
查找滾動(dòng)父元素時(shí),有一個(gè)細(xì)節(jié)就是會(huì)從自身開始查找,這也就是我們上面的 demo
中可以將指令宿主元素賦值給滾動(dòng)元素自身的原因:
// 從自身開始,尋找設(shè)置了滾動(dòng)的父元素。 overflow-y 為scroll或auto var getScrollEventTarget = function(element) { var currentNode = element; // bugfix, see http://w3help.org/zh-cn/causes/SD9013 and http://stackoverflow.com/questions/17016740/onscroll-function-is-not-working-for-chrome // nodeType 1表示元素節(jié)點(diǎn) while (currentNode && currentNode.tagName !== 'HTML' && currentNode.tagName !== 'BODY' && currentNode.nodeType === 1) { var overflowY = getComputedStyle(currentNode).overflowY; if (overflowY === 'scroll' || overflowY === 'auto') { return currentNode; } currentNode = currentNode.parentNode; } return window; };
doCheck
這個(gè)函數(shù)用于判斷是否已經(jīng)滾動(dòng)到底部,可以說(shuō)是整個(gè)插件的核心邏輯。由于滾動(dòng)的元素可以是自身,也可以是某個(gè)父元素,所以判斷會(huì)分成兩個(gè)分支。
var doCheck = function(force) { var scrollEventTarget = this.scrollEventTarget; // 滾動(dòng)父元素 var element = this.el; var distance = this.distance; // 距離閾值 if (force !== true && this.disabled) return; var viewportScrollTop = getScrollTop(scrollEventTarget); // 被隱藏在內(nèi)容區(qū)上方的像素?cái)?shù) // viewportBottom: 元素底部與文檔坐標(biāo)頂部的距離; visibleHeight:元素不帶邊框的高度 var viewportBottom = viewportScrollTop + getVisibleHeight(scrollEventTarget); var shouldTrigger = false; // 滾動(dòng)元素就是自身 if (scrollEventTarget === element) { // scrollHeight - 在沒有滾動(dòng)條的情況下,元素內(nèi)容的總高度,是元素的內(nèi)容區(qū)加上內(nèi)邊距再加上任何溢出內(nèi)容的尺寸。 // shouldTrigger為true表示已經(jīng)滾動(dòng)到元素的足夠底部了。 // 參考https://hellogithub2014.github.io/2017/10/19/dom-element-size-summary/ shouldTrigger = scrollEventTarget.scrollHeight - viewportBottom <= distance; } else { // 當(dāng)前元素與不是父元素,此時(shí)通常意味著當(dāng)前元素的高度比滾動(dòng)父元素要高,這樣父元素才會(huì)出現(xiàn)滾動(dòng) // getElementTop(element) - getElementTop(scrollEventTarget) 當(dāng)前元素頂部與滾動(dòng)父元素頂部的距離 // offsetHeight元素帶邊框的高度 // elementBottom: 元素底部與文檔坐標(biāo)頂部的距離 var elementBottom = getElementTop(element) - getElementTop(scrollEventTarget) + element.offsetHeight + viewportScrollTop; shouldTrigger = viewportBottom + distance >= elementBottom; } if (shouldTrigger && this.expression) { this.expression(); // 觸發(fā)綁定的無(wú)限滾動(dòng)函數(shù),通常是獲取下一頁(yè)數(shù)據(jù)。 之后scrollEventTarget.scrollHeight會(huì)變大 } };
這里涉及到了多種尺寸值,包括 scrollTop
、 offsetTop
、 clientHeight
、 scrollHeight
等等,如果不清楚的話整個(gè)函數(shù)的邏輯就很難看懂,關(guān)于它們的具體意義可以參考我之前寫的一篇博客。
這里我用兩幅圖來(lái)輔助理解上面的邏輯,相信會(huì)好懂很多。
滾動(dòng)元素是自身
如下,我們的目標(biāo)是判斷元素是否已滾動(dòng)到底部的距離閾值之內(nèi),很容易可以看出來(lái),距離內(nèi)容底部的距離公式為:
const { scrollHeight, clientHeight, scrollTop } = scrollEventTarget; const currentDistance = scrollHeight - clientHeight - scrollTop;
這也就是函數(shù) if
分支的邏輯,當(dāng) currentDistance
小于 distance
時(shí),我們就可以加載下一頁(yè)數(shù)據(jù)了。
父級(jí)元素設(shè)置滾動(dòng)
此時(shí)就沒有 scrollTop
屬性可以操作了,但是元素的高度仍然可以用上面的屬性:滾動(dòng)父元素的高度可以用 scrollEventTarget.clientHeight
,子元素內(nèi)容高度可以用 element.offsetHeight
,剩下的就是計(jì)算 topGap
了。
我們知道 DOM
的坐標(biāo)有兩種:文檔坐標(biāo)、視口坐標(biāo),計(jì)算 topGap
只要始終在其中一個(gè)坐標(biāo)系計(jì)算就可以了,這里我們采用視口坐標(biāo)。 ele.getBoundingClientRect().top
可以知道一個(gè)元素距離視口頂部的距離,那么 topGap
的計(jì)算公式就是:
const topGap = scrollEventTarget.getBoundingClientRect().top - element.getBoundingClientRect().top;
綜上,子元素底部與父元素底部的距離公式就是:
const currentDistance = element.offsetHeight - scrollEventTarget.clientHeight - (scrollEventTarget.getBoundingClientRect().top - element.getBoundingClientRect().top);
這也就是函數(shù)的 else
分支邏輯。
以上是“無(wú)限滾動(dòng)插件vue-infinite-scroll的示例分析”這篇文章的所有內(nèi)容,感謝各位的閱讀!相信大家都有了一定的了解,希望分享的內(nèi)容對(duì)大家有所幫助,如果還想學(xué)習(xí)更多知識(shí),歡迎關(guān)注億速云行業(yè)資訊頻道!
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。