国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁 > 編程 > JavaScript > 正文

詳解無限滾動插件vue-infinite-scroll源碼解析

2019-11-19 11:35:46
字體:
來源:轉載
供稿:網友

最近在項目中遇到一個需求,有一個列表需要滾動加載,類似于微博的無限滾動。當時第一反應時監聽滾動事件,在判斷滾動到達底部時加載下一頁,同時心里也清楚,監聽滾動事件需要做好截流。順手搜索了下發現有一個現成的插件vue-infinite-scroll ,用法也很簡單,于是乎就用了起來。 需求上線后,對它的實現挺好奇的,于是研究了一番源碼,這篇文章就是源碼解析筆記。

插件使用方法

這是一個 vue 的指令,按照 github 倉庫上的介紹,用法挺簡單的,例如:

<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);  }, },});

這里的指令宿主元素自身設置了 overflow:auto ,內部元素用來支撐滾動,當滾動到底部時,增加內部元素的高度從而模擬了無限滾動。效果如下:

另外可以將父元素設置為滾動,當自身滾動到父元素底部時,增加自身的高度,模擬拉取下一頁數據的操作。 例如:

<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>

達到的效果和上面完全相同。

源碼解析

接下來就是看看內部怎么實現的。照例從入口開始看起。因為這個插件就是一個 vue 的指令,所以入口還是挺簡單的:

指令入口

export default { bind(el, binding, vnode) {  el[ctx] = {   el,   vm: vnode.context,   expression: binding.value, // 滾動到底部時需要的監聽函數,通常用于加載下一頁數據  };  const args = arguments;  // 監聽宿主元素所在組件的mounted事件  el[ctx].vm.$on('hook:mounted', function() {   el[ctx].vm.$nextTick(function() {    // 判斷元素是否已經在頁面上    if (isAttached(el)) {     // 獲取各項指令相關屬性,執行各種事件綁定     doBind.call(el[ctx], args);    }    el[ctx].bindTryCount = 0;    // 間隔50ms輪訓10次,判斷元素是否已經在頁面上    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); },};

核心就是在宿主元素渲染后,執行 doBind 方法,我們猜測會在 doBind 綁定滾動父元素的 scroll 事件。

isAttached 方法用于判斷一個元素是否已渲染在頁面上,判斷方法是查看是否有組件元素的標簽名為 HTML

// 判斷元素是否已經在頁面上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;};

參數解析與事件綁定

現在看看 doBind 方法,邏輯比較多,不過都不難。

var doBind = function() { if (this.binded) return; // 只綁定一次 this.binded = true; var directive = this; var element = directive.el; // throttleDelayExpr: 截流間隔。 設置在元素的屬性上 var throttleDelayExpr = element.getAttribute('infinite-scroll-throttle-delay'); var throttleDelay = 200; if (throttleDelayExpr) {  // 優先嘗試組件上的throttleDelayExpr屬性值, 如 <div infinite-scroll-throttle-delay="myDelay"></div>  throttleDelay = Number(directive.vm[throttleDelayExpr] || throttleDelayExpr);  if (isNaN(throttleDelay) || throttleDelay < 0) {   throttleDelay = 200;  } } directive.throttleDelay = throttleDelay; // 監聽滾動父元素的scroll時間,監聽函數設置了函數截流 directive.scrollEventTarget = getScrollEventTarget(element); // 設置了滾動的父元素 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: 是否禁用無限滾動 // 可以為表達式 var disabledExpr = element.getAttribute('infinite-scroll-disabled'); var disabled = false; if (disabledExpr) {  this.vm.$watch(disabledExpr, function(value) {   directive.disabled = value;   // 當disable為false時,重啟check   if (!value && directive.immediateCheck) {    doCheck.call(directive);   }  });  disabled = Boolean(directive.vm[disabledExpr]); } directive.disabled = disabled; // 宿主元素到滾動父元素底部的距離閾值,小于這個值時,觸發listen-for-event監聽函數 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后立即檢查一遍,也會在disable失效時立即觸發檢查 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); } // 當組件上設置的此事件觸發時,執行一次檢查 var eventName = element.getAttribute('infinite-scroll-listen-for-event'); if (eventName) {  directive.vm.$on(eventName, function() {   doCheck.call(directive);  }); }};

整個看下來,核心就是利用各種參數控制 doCheck 的調用,包括時間間隔、 disabled 、距離閾值、 immediate-check 、組件事件。

doCheck 因為會非常頻繁的調用,所以用 throttle 進行了截流,具體邏輯這里不再贅述。

getScrollEventTarget 查找滾動父元素時,有一個細節就是會從自身開始查找,這也就是我們上面的 demo 中可以將指令宿主元素賦值給滾動元素自身的原因:

// 從自身開始,尋找設置了滾動的父元素。 overflow-y 為scroll或autovar 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表示元素節點 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

這個函數用于判斷是否已經滾動到底部,可以說是整個插件的核心邏輯。由于滾動的元素可以是自身,也可以是某個父元素,所以判斷會分成兩個分支。

var doCheck = function(force) { var scrollEventTarget = this.scrollEventTarget; // 滾動父元素 var element = this.el; var distance = this.distance; // 距離閾值 if (force !== true && this.disabled) return; var viewportScrollTop = getScrollTop(scrollEventTarget); // 被隱藏在內容區上方的像素數 // viewportBottom: 元素底部與文檔坐標頂部的距離; visibleHeight:元素不帶邊框的高度 var viewportBottom = viewportScrollTop + getVisibleHeight(scrollEventTarget); var shouldTrigger = false; // 滾動元素就是自身 if (scrollEventTarget === element) {  // scrollHeight - 在沒有滾動條的情況下,元素內容的總高度,是元素的內容區加上內邊距再加上任何溢出內容的尺寸。  // shouldTrigger為true表示已經滾動到元素的足夠底部了。  // 參考https://hellogithub2014.github.io/2017/10/19/dom-element-size-summary/  shouldTrigger = scrollEventTarget.scrollHeight - viewportBottom <= distance; } else {  // 當前元素與不是父元素,此時通常意味著當前元素的高度比滾動父元素要高,這樣父元素才會出現滾動  // getElementTop(element) - getElementTop(scrollEventTarget) 當前元素頂部與滾動父元素頂部的距離  // offsetHeight元素帶邊框的高度  // elementBottom: 元素底部與文檔坐標頂部的距離  var elementBottom = getElementTop(element) - getElementTop(scrollEventTarget) + element.offsetHeight + viewportScrollTop;  shouldTrigger = viewportBottom + distance >= elementBottom; } if (shouldTrigger && this.expression) {  this.expression(); // 觸發綁定的無限滾動函數,通常是獲取下一頁數據。 之后scrollEventTarget.scrollHeight會變大 }};

這里涉及到了多種尺寸值,包括 scrollTopoffsetTopclientHeightscrollHeight 等等,如果不清楚的話整個函數的邏輯就很難看懂,關于它們的具體意義可以參考我之前寫的一篇博客。

這里我用兩幅圖來輔助理解上面的邏輯,相信會好懂很多。

滾動元素是自身

 如下,我們的目標是判斷元素是否已滾動到底部的距離閾值之內,很容易可以看出來,距離內容底部的距離公式為:

const { scrollHeight, clientHeight, scrollTop } = scrollEventTarget;const currentDistance = scrollHeight - clientHeight - scrollTop;

這也就是函數 if 分支的邏輯,當 currentDistance 小于 distance 時,我們就可以加載下一頁數據了。

父級元素設置滾動

此時就沒有 scrollTop 屬性可以操作了,但是元素的高度仍然可以用上面的屬性:滾動父元素的高度可以用 scrollEventTarget.clientHeight ,子元素內容高度可以用 element.offsetHeight ,剩下的就是計算 topGap 了。

我們知道 DOM 的坐標有兩種:文檔坐標、視口坐標,計算 topGap 只要始終在其中一個坐標系計算就可以了,這里我們采用視口坐標。 ele.getBoundingClientRect().top 可以知道一個元素距離視口頂部的距離,那么 topGap 的計算公式就是:

const topGap = scrollEventTarget.getBoundingClientRect().top - element.getBoundingClientRect().top;

綜上,子元素底部與父元素底部的距離公式就是:

const currentDistance = element.offsetHeight - scrollEventTarget.clientHeight - (scrollEventTarget.getBoundingClientRect().top - element.getBoundingClientRect().top);

這也就是函數的 else 分支邏輯。

以上就是 doCheck 的核心檢測邏輯了,同時針對 scrollEventTargetdocument 時做了一些特殊處理,留給大家自己去看。

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持武林網。

發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
主站蜘蛛池模板: 绥中县| 桃源县| 衡阳县| 陵川县| 青阳县| 茶陵县| 沁源县| 宁远县| 乃东县| 得荣县| 新郑市| 荣成市| 苏尼特右旗| 昌宁县| 平潭县| 石首市| 东乌珠穆沁旗| 宁晋县| 六安市| 耒阳市| 革吉县| 义乌市| 会东县| 海林市| 南丰县| 永嘉县| 厦门市| 太保市| 曲沃县| 同心县| 临猗县| 观塘区| 泗阳县| 专栏| 昌黎县| 洛南县| 清徐县| 资兴市| 新野县| 怀宁县| 宁城县|