基於element-ui el-table 開發虛擬列表(樹形列表)

前言

在這裏插入圖片描述
基於以前支持表單驗證的el-table開發完成後,在數據量過大的時候,會出現渲染慢,表格卡頓等致命問題,而element-ui的el-table自己沒有像antd同樣提供虛擬列表的demo和相關支持,所以本文在上次的開發基礎上,繼而開展虛擬列表的開發。本次分爲普通列表和樹形列表兩種,樹形在普通列表上面多了一些狀況考慮,例如展開收縮等。javascript

虛擬列表

虛擬列表簡單概述就是滾動分頁,經過有限的視口來切片大量的數據,由於相比於js運算,渲染是一個很慢的過程,所以經過必定的js計算,保證更少的數據渲染,一般能夠得到更好的用戶體驗。通常虛擬列表能夠經過上下的動態padding值,來是滾動區域一直顯示當前切片出的數據,以及經過transform的方法來動態移動可視區。transform這種方法理論上性能要好一些,由於瀏覽器渲染自己是分圖層渲染的,而transform操做的視圖,會被瀏覽器單獨分層出來,渲染性能更優。
下圖兩種方式:
在這裏插入圖片描述
本次el-table 上的虛擬列表,採用了padding的方案,緣由是transform 會使el-table的樣式混亂,若是是本身開發的table或者其餘魔改支持度較好的插件的話優先transform。java

普通列表

首先看一下el-table 渲染300 條的速度。
在這裏插入圖片描述
本次的測試代碼有300條,自己並很少,可是有8列都是插槽中渲染的表單組件,所以渲染速度要慢不少,時間花銷6s+。(antd 的table渲染要快一些,後面說緣由)node

增長虛擬列表後渲染速度:
在這裏插入圖片描述element-ui

開發流程

step1

計算總高度數組

height = list.length * 65 
// height 爲列表實際總高度
// 65 爲每一行的行高,根據實際修改
// list爲實際數據長度

step2

計算上下padding值瀏覽器

paddingTop = scrollTop + "px";
paddingBottom = height - 10 * 65 - scrollTop + "px";
// scrollTop 爲滾動的高度,即列表向下滾動的距離
// height 總高度 
// 10爲實際渲染的條數

step3

監聽列表滾動,動態爲列表設置padding等樣式。緩存

mounted() {
    console.time("render300條時間:");
    this.form.rows = new Array(300).fill(0).map((v, i) => ({
      name: i,
      children: []
    }));
    this.form.rows = [...this.form.rows];
    this.setIndex(this.form.rows);
    this.calcList();
    this.$nextTick(() => {
      this.debounceFn = _.debounce(() => {
        this.scrollTop = this.$refs.table.bodyWrapper.scrollTop;
      }, 100);
      this.$refs.table.bodyWrapper.addEventListener("scroll", this.debounceFn);
    });
    this.$nextTick(() => {
      console.timeEnd("render300條時間:");
    });
  },

監聽的目標是這個: this.$refs.table.bodyWrapper,防抖的時間設置爲100。antd

step4

數組切片,渲染虛擬列表。數據結構

this.startIndex = Math.floor(scrollTop / 65);
 this.virtualRows = this.form.rows.slice(
        this.startIndex,
        this.startIndex + 10
      );

根據滾動位置計算數組切片的起始點,而後截取相應的list渲染。app

支持列fixed(Table-column Attributes - fixed)

上面說到el-table要比antd的table渲染更慢,其中一條緣由我我的認爲是,el-table 在支持左右固定列的時候會克隆一份table,而後按照層級關係,使得UI上看到左右列的固定。若是左右都設置了fixed,就會有三個table同時在頁面上。
在這裏插入圖片描述
而 Antd的table組件在左右fixed時就不會有這個問題,所以本人親測在300條相同數據的情境下,Antd的性能要好很多。言歸正傳,要解決fixed的問題,就是要把這三個table的padding都去設置一遍才行,不然就會出現部分區域沒有被頂下來而錯位的狀況。

let mainTable = this.$refs.table.$el.getElementsByClassName(
        "el-table__body"
      );
      Array.from(mainTable).forEach(v => {
        v.style.height = height + "px";
        if (this.startIndex + 10 >= this.num) {
          // 因爲el-table 在滾動到最後時,會出現抖動,所以增長判斷,單獨設置屬性
          v.style.paddingTop = scrollTop - 65 + "px";
          v.style.paddingBottom = 0;
        } else {
          v.style.paddingTop = scrollTop + "px";
          v.style.paddingBottom = height - 10 * 65 - scrollTop + "px";
        }
      });

找到當前table下的全部內容區域,遍歷設置樣式屬性。

樹形列表

樹形列表因爲多一步展開摺疊的操做,以及自己數據結構的緣由,數據預處理要複雜一下,不能直接slice,而要計算出相應區間而後生成新的數組。其次,在被收縮的子項是不渲染到table當中的,所以,要把被收縮的項排除在外。除了普通列表的幾個step以外,樹形列表還需有如下操做。

數組切片

經過滾動計算出的起始點,以及可視區域的列表長度,能夠獲得一個區間,如【3,11】,即經過深度優先遍歷(也是樹形列表排列的順序),找到第3到11條數據(不包含被摺疊項),而後賦值到新的數組。

clacTree() {
      let count = 0;
      this.virtualRows = [];
      this.listLen = 0;
      const fn = arr => {
        for (let i = 0; i < arr.length; i++) {
          count++;
          this.listLen++;
          if (count >= this.startIndex && count <= this.startIndex + 10) {
            this.combineArr(_.cloneDeep(arr[i]));
          }
          arr[i].children && arr[i].expended === "true" && fn(arr[i].children);
        }
      };
      fn(this.form.rows);
    },
    combineArr(node) {
      let flag = false;
      node.children = [];
      const fn = arr => {
        arr.forEach(v => {
          if (node.pid === v.customIndex) {
            v.children.push(node);
            flag = true;
          }
          v.children && fn(v.children);
        });
      };
      fn(this.virtualRows);
      if (!flag) {
        this.virtualRows.push(node);
      }
    },

這裏只對展開項進行操做,未展開的不去遍歷和渲染,總高度也不計入。新數組賦值的時候,我經過二次遍歷新的數組,再根據pid去push到相應位置,這種作法是由於實際業務須要,二次遍歷中還有部分屬性須要保持引用,以及部分屬性是不可枚舉的,深拷貝會丟失,若是隻是截取樹的一部分造成新的樹,能夠根據初始化獲得的path屬性,而後利用lodash的_.set 來完成。

展開收縮

el-table 的 expand-row-keys 傳入一個數組,爲默認的展開項,以後每次渲染都參考這個數組來決定列表是否展開,這個屬性不能自動在展開收縮的時候把設置的 row-key 推入推出,而要手動的計算。在@expand-change事件中,來操做數組,以及判斷被收縮的項有沒有子集,若是有子集要給一個標記位,來爲以後的列表渲染作準備。

expendRow(rows, expended) {
      // const
      this.DFS_Array(this.form.rows, v => {
        if (v.customIndex == rows.customIndex) {
          v.expended = String(expended);
          v.hasChild =
            v.expended === "false" && v.children.length > 0 ? true : false;
        }
      });
      if (!expended) {
        this.expendArrs = this.expendArrs.filter(v => v !== rows.customIndex);
      } else {
        this.expendArrs.push(rows.customIndex);
      }
      this.calcList(this.scrollTop);
  },
  DFS_Array(arr, fn) {
      for (let i = 0; i < arr.length; i++) {
        fn(arr[i]);
        if (arr[i].children && arr[i].children.length > 0) {
          this.DFS_Array(arr[i].children, fn);
        }
      }
    }

在收縮以後因爲把列表中的children整個移除,因此在el-table上面的展開箭頭就不能正常顯示了,由於在渲染數據中並無子節點,而實際數據中又是有子集的,因此,在上面增長的hasChild屬性,就起到這個做用,他標記了數據被摺疊,且有子集可展開的狀況。所以,須要在列表中主動把展開的箭頭加一下。

<el-table-column
            prop="customIndex"
            fixed
            label="序號"
            sortable
            width="180"
            v-slot="{$index, row}"
          >
            <span class="expanded-icon-box">
              <i class="expanded-icon" v-if="row.hasChild" @click="expendRow(row,true)">></i>
              {{row.customIndex}}
            </span>
          </el-table-column>

至此 樹形列表的虛擬列表也整合完畢了。本次示例代碼不少地方比較倉促,待優化情景較多,除了拼湊新的樹那裏,還有滾動的緩存,若是樹比較大的話,js的計算時間也要考慮入內,還有渲染的虛擬列表應該不從自己的第一位開始進入視口,這樣的話,在必定範圍的向上向下滾動,就能夠必定程度的減小白屏。

總結

虛擬列表經過減小實際渲染數據來優化性能,在不對element-ui作較大改動的狀況下,知足了大量數據,包括樹形的結構數據的渲染場景。若是考慮以前的列表的表單驗證的情景,須要讓部分屬性脫離引用,如children,不然會污染源數據,其次讓表單數據保持引用關聯,這樣就沒必要專門給表單組件設置事件,來匹配源數據的改動,即直接將新的列表的item的表單對象等於老的相應表單對象便可。

相關文章
相關標籤/搜索