基于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);
})
});
}
};
注释比较简单,要注释的地方太多了,就看代码吧。还有很多值得优化的地方。
代码中使用的是全局注册指令,我就不贴代码了
可以尝试用这个整合下Element UI https://clusterize.js.org/
看他官网的例子,生成10W+级别的数据滑动也丝毫不卡顿,用的类似卷帘的技术,超出的部分自动移除了。