1. 程式人生 > >vue mint-ui原始碼解析之loadmore元件

vue mint-ui原始碼解析之loadmore元件

  • 元件的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 = '';
        }
      }