VUE,ElementUI列表大数据卡顿问题?(已解决)

基于elementUI的table,在不修改源码的情况下支持大数据了渲染的场景

2019-7-18又优化了一版(后面遇到BUG,就直接更改这个了),相当于重写,用了指令的一些特性;完善了下注释


var EventUtil = {
  /*检测绑定事件*/
  addHandler: function (element, type, handler) {
    if (element.addEventListener) {
      element.addEventListener(type, handler, false);
    } else if (element.attachEvent) {
      element.attachEvent('on' + type, handler);
    } else {
      element["on" + type] = handler /*直接赋给事件*/
    }
  },
  /*通过removeHandler*/
  removeHandler: function (element, type, handler) {
    /*Chrome*/
    if (element.removeEventListener)
      element.removeEventListener(type, handler, false);
    else if (element.deattachEvent) {
      /*IE*/
      element.deattachEvent('on' + type, handler);
    } else {
      element["on" + type] = null;
      /*直接赋给事件*/
    }
  }
};

// 设置默认溢出显示数量
var spillDataNum = 5;

// 设置隐藏函数
let timeout = false;
let triggerDataIndexChange = function (el, binding, vnode, oldVnode, currentTableHeight) {
  if (timeout) {
    clearTimeout(timeout);
  }
  const elTableBodyWrapper = el.querySelector('.el-table__body-wrapper');
  const virtualDiv = elTableBodyWrapper.querySelector('div.virtual-div');
  const elTableBody = elTableBodyWrapper.querySelector('table.el-table__body');
  const elTableRow = elTableBodyWrapper.querySelector('table tr.el-table__row');
  const context = vnode.context;
  const tableData = context.tableData;

  const elTableBodyWrapperHeight = elTableBodyWrapper.clientHeight;
  const elTableRowHeight = elTableRow.clientHeight;
  const showRowNum = Math.round(elTableBodyWrapperHeight / elTableRowHeight);

  let maxScrollTop = virtualDiv.clientHeight - elTableBodyWrapper.clientHeight - spillDataNum * elTableRowHeight;
  // 处理滚动条置顶的情况
  if (elTableBodyWrapper.scrollTop === 0) {
    context.currentStartIndex = 0;
    context.currentEndIndex = tableData.length + spillDataNum;
    elTableBody.setAttribute('style', 'transform: translateY(0px);width: ' + elTableBody.style.width + ';');
    return;
  }

  // 处理滚动条滚动中和置低的情况
  let scrollTopNum = elTableBodyWrapper.scrollTop / elTableRowHeight;
  let currentStartIndex = scrollTopNum;
  let currentEndIndex = scrollTopNum + showRowNum + spillDataNum;
  if (currentEndIndex - tableData.length >= 0) {
    context.currentStartIndex = tableData.length - 1 - showRowNum - spillDataNum;
    context.currentEndIndex = tableData.length - 1;
    elTableBody.setAttribute('style', 'transform: translateY(' + maxScrollTop + 'px);width: ' + elTableBody.style.width + ';');
    return;
  }
  context.currentStartIndex = currentStartIndex;
  context.currentEndIndex = currentEndIndex;
  elTableBody.setAttribute('style', 'transform: translateY(' + (elTableBodyWrapper.scrollTop) + 'px);width: ' + elTableBody.style.width + ';');
};

// 滚动条置低
let handleScrollEnd = function (el) {
  const elTableBodyWrapper = el.querySelector('.el-table__body-wrapper');
  elTableBodyWrapper.scrollTop = elTableBodyWrapper.scrollHeight;
};

export default {
  name: 'loadmore',
  inserted: function (el, binding, vnode, oldVnode) {
    const isLoadmore = vnode.data.attrs['is-loadmore'];
    if (isLoadmore === 'NoScrollBar') {
      return;
    }
    const elTableBodyWrapper = el.querySelector('.el-table__body-wrapper');
    const elTableBodyWrapperChildren = elTableBodyWrapper.children;
    // 创建虚拟DIV,用来设置数据高度,触发滚动条高度更新
    const virtualDiv = document.createElement('div');
    virtualDiv.setAttribute('class', "virtual-div");
    elTableBodyWrapper.appendChild(virtualDiv);
    virtualDiv.appendChild(elTableBodyWrapperChildren[0]);
    // 滚动事件
    let handleMoveScroll = function () {
      triggerDataIndexChange(el, binding, vnode, oldVnode);
    }
    EventUtil.addHandler(elTableBodyWrapper, "scroll", handleMoveScroll);
  },
  update() {},
  componentUpdated: function (el, binding, vnode, oldVnode) {
    // 获取组件缓存的数据
    const dataSize = vnode.data.attrs['data-size'];
    const oldDataSize = oldVnode.data.attrs['data-size'];
    const isLoadmore = vnode.data.attrs['is-loadmore'];
    const isAddRowData = vnode.data.attrs['is-addRowData'];

    // 获取必要的DOM节点
    const elTableBodyWrapper = el.querySelector('.el-table__body-wrapper');
    const elTableRow = elTableBodyWrapper.querySelector('table tr.el-table__row');
    let elTableRowHeight = 0;

    if (elTableRow) {
      elTableRowHeight = elTableRow.clientHeight;
    }
    if (elTableRowHeight === 0) {
      elTableRowHeight = 25;
    }

    // 如果是设置不需要滚动条的模式,直接显示所有数据,并设置表格的高度为数据的高度
    if (isLoadmore === 'NoScrollBar') {
      const context = vnode.context;
      let tableHeight = (dataSize + 1) * elTableRowHeight + 40;
      context.currentTableHeight = tableHeight;
      return;
    }

    if (dataSize) {
      // 根据行高计算当前应该显示那些数据
      // 根据数据多少和行高设置虚拟滚动DIV的高度
      const virtualDiv = elTableBodyWrapper.querySelector('div.virtual-div');
      const virtualDivHeight = dataSize * elTableRowHeight;
      virtualDiv.setAttribute('style', `height: ${virtualDivHeight}px;`);
      // 首次进来,即currentStartIndex为零,如果可显示区域大于默认显示20条数据。要重新赋值默认显示的数量为显示区域的数量加上默认溢出的数量
      const bodyHeight = vnode.componentInstance.layout.height;
      const showRowNum = Math.round(bodyHeight / elTableRowHeight);
      const currentStartIndex = vnode.context.currentStartIndex;
      const currentEndIndex = vnode.context.currentEndIndex;
      if (currentStartIndex === 0 && bodyHeight && showRowNum > currentEndIndex) {
        vnode.context.currentEndIndex = showRowNum + spillDataNum;
      }
    }

    // 新增数据处理,直接将滚动条置底
    if (isAddRowData) {
      handleScrollEnd(el);
    }
  }
};

问题描述

很早之前的问题,后来根据网友的回答,又优化了一些版本。今天整理下,感谢寻梦无痕提供的帮助

场景

前端UI框架使用的是ElementUI,项目要求数据不分页一个表格至少要1000条数据,这时点击其他DOM操作,会出现卡顿的现象。如点击复选框。

现状

已解决3000条数据点击其他DOM操作卡顿的问题。

求助

助如何解决初次放入数据时卡顿的情况?(内附解决加载完成后卡顿的思路)**

结果分享

优化了大概三个版本,保留第一次的分享的,后面直接贴出最终版
解决3000条数据点击其他DOM操作卡顿的思路
通过指令添加表格滚动后的事件,设置非显示区域的其他DOM节点隐藏。

指令代码

// 设置隐藏函数
var timeout = false;
let setRowDisableNone = function (selectRows, topNum, rowHeight, showRowNum) {
  if (timeout) {
    clearTimeout(timeout);
  }
  timeout = setTimeout(() => {
    selectRows.forEach((selectRow, index) => {
      const selectRowsTds = selectRow.querySelectorAll('td');
      if (topNum <= index && index <= (topNum + showRowNum)) {
        selectRowsTds.forEach((selectRowsTd) => {
          selectRowsTd.style.display = '';
        });
      } else {
        selectRow.style.height = `${rowHeight}px`;
        selectRowsTds.forEach((selectRowsTd) => {
          selectRowsTd.style.display = 'none';
        });
      }
    });
  }, 100);
};

export default {
  name: 'loadmore',
  componentUpdated: function (el) {
    setTimeout(() => {
      const selectWrap = el.querySelector('.el-table__body-wrapper');
      const selectRow = selectWrap.querySelector('table tr');
      const rowHeight = selectRow.clientHeight;
      const selectRows = selectWrap.querySelectorAll('table tr');
      setRowDisableNone(selectRows, 0, rowHeight, showRowNum);
      // 监听滚动事件,操作DOM隐藏和显示
      selectWrap.addEventListener('scroll', function () {
        let topPx = this.scrollTop;
        let topNumber = Math.round(topPx / rowHeight);
        let rowTopLastIndex = topNumber;
        setRowDisableNone(selectRows, rowTopLastIndex, rowHeight, showRowNum);
      })
    });
  }
};

有同学说componentUpdated执行不了,这个我是通过指令做的,后面我贴出完成的代码和例子

解决性能问题(最终版)

参考评论区寻梦无痕分享的例子,整合以下,感谢。

思路

减少对DOM节点的渲染,通过滚动函数节流实现滚动后事件来动态渲染数据

Element Table代码

<template>
  <div>
    <el-table border :data="filteredData" style="width: 100%" height="300" :data-size="tableData.length" v-loadmore="handelLoadmore">
      <el-table-column type="selection" width="55">
      </el-table-column>
      <el-table-column label="日期" width="180">
        <template slot-scope="scope">
          <div>
            <i class="el-icon-time"></i>
            <span style="margin-left: 10px">{{ scope.row.date }}</span>
          </div>
        </template>
      </el-table-column>
      <el-table-column label="日期" width="180">
        <template slot-scope="scope">
          <div>
            <i class="el-icon-time"></i>
            <span style="margin-left: 10px">{{ scope.row.date }}</span>
          </div>
        </template>
      </el-table-column>
      <el-table-column label="日期" width="180">
        <template slot-scope="scope">
          <div>
            <i class="el-icon-time"></i>
            <span style="margin-left: 10px">{{ scope.row.date }}</span>
          </div>
        </template>
      </el-table-column>
      <el-table-column label="日期" width="180">
        <template slot-scope="scope">
          <div>
            <i class="el-icon-time"></i>
            <span style="margin-left: 10px">{{ scope.row.date }}</span>
          </div>
        </template>
      </el-table-column>
      <el-table-column label="姓名" width="180">
        <template slot-scope="scope">
          <div>
            <el-popover trigger="hover" placement="top">
              <p>姓名: {{ scope.row.name }}</p>
              <p>住址: {{ scope.row.address }}</p>
              <div slot="reference" class="name-wrapper">
                <el-tag size="medium">{{ scope.row.name }}</el-tag>
              </div>
            </el-popover>
          </div>
        </template>
      </el-table-column>
      <el-table-column label="操作">
        <template slot-scope="scope">
          <div>
            <el-button size="mini"  @click="handleEdit(scope.$index, scope.row)">编辑</el-button>
            <el-button size="mini" type="danger"  @click="handleDelete(scope.$index, scope.row)">删除</el-button>
          </div>
        </template>
      </el-table-column>
    </el-table>
  </div>
</template>

<script>

export default {
  name: 'test',
  components: {},
  data () {
    return {
      tableData: [],
      currentStartIndex: 0,
      currentEndIndex: 20
    };
  },
  created () {
    this.getTableData();
  },
  computed: {
    filteredData () {
      return this.tableData.filter((item, index) => {
        if (index < this.currentStartIndex) {
          return false;
        } else if (index > this.currentEndIndex) {
          return false;
        } else {
          return true;
        }
      });
    }
  },
  methods: {
    handelLoadmore (currentStartIndex, currentEndIndex) {
      this.currentStartIndex = currentStartIndex;
      this.currentEndIndex = currentEndIndex;
    },
    getTableData () {
      let cont = 0;
      let tableData = [];
      while (cont < 30000) {
        cont = cont + 1;
        let object = {
          date: cont,
          name: '王小虎' + cont,
          address: '上海市普陀区金沙江路 cont 弄'
        }
        tableData.push(object);
      }
      setTimeout(() => {
        this.tableData = tableData;
      }, 2000);
    }
  },
  watch: {}
}
</script>

<style scoped>
.el-table__body-wrapper .el-table__row td {
  display: none;
}
.el-table__body-wrapper .el-table__row {
  height: 38px;
}
</style>

指令代码

// 设置默认溢出显示数量
var spillDataNum = 20;

// 设置隐藏函数
var timeout = false;
let setRowDisableNone = function (topNum, showRowNum, binding) {
  if (timeout) {
    clearTimeout(timeout);
  }
  timeout = setTimeout(() => {
    binding.value.call(null, topNum, topNum + showRowNum + spillDataNum);
  });
};

export default {
  name: 'loadmore',
  componentUpdated: function (el, binding, vnode, oldVnode) {
    setTimeout(() => {
      const dataSize = vnode.data.attrs['data-size'];
      const oldDataSize = oldVnode.data.attrs['data-size'];
      if(dataSize === oldDataSize){
        return;
      }
      const selectWrap = el.querySelector('.el-table__body-wrapper');
      const selectTbody = selectWrap.querySelector('table tbody');
      const selectRow = selectWrap.querySelector('table tr');
      if (!selectRow) {
        return;
      }
      const rowHeight = selectRow.clientHeight;
      let showRowNum = Math.round(selectWrap.clientHeight / rowHeight);

      const createElementTR = document.createElement('tr');
      let createElementTRHeight = (dataSize - showRowNum - spillDataNum) * rowHeight;
      createElementTR.setAttribute('style', `height: ${createElementTRHeight}px;`);
      selectTbody.append(createElementTR);

      // 监听滚动后事件
      selectWrap.addEventListener('scroll', function () {
        let topPx = this.scrollTop - spillDataNum * rowHeight;
        let topNum = Math.round(topPx / rowHeight);
        let minTopNum = dataSize - spillDataNum - showRowNum;
        if (topNum > minTopNum) {
          topNum = minTopNum;
        }
        if (topNum < 0) {
          topNum = 0;
          topPx = 0;
        }
        selectTbody.setAttribute('style', `transform: translateY(${topPx}px)`);
        createElementTR.setAttribute('style', `height: ${createElementTRHeight-topPx > 0 ? createElementTRHeight-topPx : 0}px;`);
        setRowDisableNone(topNum, showRowNum, binding);
      })
    });
  }
};

注释比较简单,要注释的地方太多了,就看代码吧。还有很多值得优化的地方。

代码中使用的是全局注册指令,我就不贴代码了

阅读 70.9k
15 个回答

可以尝试用这个整合下Element UI https://clusterize.js.org/
看他官网的例子,生成10W+级别的数据滑动也丝毫不卡顿,用的类似卷帘的技术,超出的部分自动移除了。

不管怎么处理,只要页面上的数据元素很大,卡是必然的。
你分段加载最后界面上依然是很多元素。
chrome中如果感觉有点卡的话,IE估计就没法用了....
之前做的elementUI下拉选择2000多条卡成狗,最后换成了远程搜索。

发现个问题,当‘操作’列固定在右边时,这个table的数据滚动到底部列表就错位了
image

分段加载, 先加载 2屏的数据, 再加载剩余的数据

你这个问题解决了吗,我现在实验你上边说的这个方法,componentUpdated里的事件根本不能执行啊,求助求助

兄弟,有没有源码呢?

新手上路,请多包涵

最近正好在优化这个table,谢谢作者

有的地方不理解啊,老哥,该讲下思路呗。

新手上路,请多包涵

好像在表格使用固定列fixed的时候,滚动的时表格会错位。

新手上路,请多包涵

可视区域全选时表头的状态为全选,这个问题遇到了嘛

新手上路,请多包涵

表格每行的高度不固定怎么办

博主,建议使用requestAnimationFrame替代setTimeOut;没有滚动效果是咋回事呢?

有办法在超出溢出数量后有滚动特效吗,目前是没有滚动特效的,楼主可否给我点主意呢,我刚接触vue不久

感谢楼主分享,但还是有些bug,感觉不太和我要求,所以目前我使用了这个插件
https://www.npmjs.com/package...

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题
宣传栏