vue antd 解决表格大量数据卡顿
vue antd 解决表格大量数据卡顿
·
<template>
<div>
<a-table
v-bind="$attrs"
v-on="$listeners"
:pagination="false"
:columns="tableColumns"
:data-source="renderData"
>
<template
v-for="slot in Object.keys($scopedSlots)"
:slot="slot"
slot-scope="text"
>
<slot
:name="slot"
v-bind="typeof text === 'object' ? text : { text }"
></slot>
</template>
</a-table>
<div class="ant-table-append" ref="append" v-show="!isHideAppend">
<slot name="append"></slot>
</div>
</div>
</template>
<script>
import throttle from "lodash/throttle";
// import Checkbox from 'ant-design-vue/lib/checkbox'
// import Table from 'ant-design-vue/lib/table'
// 判断是否是滚动容器
function isScroller(el) {
const style = window.getComputedStyle(el, null);
const scrollValues = ["auto", "scroll"];
return (
scrollValues.includes(style.overflow) ||
scrollValues.includes(style["overflow-y"])
);
}
// 获取父层滚动容器
function getParentScroller(el) {
let parent = el;
while (parent) {
if ([window, document, document.documentElement].includes(parent)) {
return window;
}
if (isScroller(parent)) {
return parent;
}
parent = parent.parentNode;
}
return parent || window;
}
// 获取容器滚动位置
function getScrollTop(el) {
return el === window ? window.pageYOffset : el.scrollTop;
}
// 获取容器高度
function getOffsetHeight(el) {
return el === window ? window.innerHeight : el.offsetHeight;
}
// 滚动到某个位置
function scrollToY(el, y) {
if (el === window) {
window.scroll(0, y);
} else {
el.scrollTop = y;
}
}
// 表格body class名称
const TableBodyClassNames = [
".ant-table-scroll .ant-table-body",
".ant-table-fixed-left .ant-table-body-inner",
".ant-table-fixed-right .ant-table-body-inner",
];
let checkOrder = 0; // 多选:记录多选选项改变的顺序
export default {
inheritAttrs: false,
name: "vt",
props: {
dataSource: {
type: Array,
default: () => [],
},
columns: {
type: Array,
default: () => [],
},
// key值,data数据中的唯一id
keyProp: {
type: String,
default: "name",
},
// 每一行的预估高度
itemSize: {
type: Number,
default: 60,
},
// 指定滚动容器
scrollBox: {
type: String,
},
// 顶部和底部缓冲区域,值越大显示表格的行数越多
buffer: {
type: Number,
default: 100,
},
// 滚动事件的节流时间
throttleTime: {
type: Number,
default: 10,
},
// 是否获取表格行动态高度
dynamic: {
type: Boolean,
default: true,
},
// 是否开启虚拟滚动
virtualized: {
type: Boolean,
default: true,
},
// 是否是树形结构
isTree: {
type: Boolean,
default: false,
},
},
data() {
return {
start: 0,
end: undefined,
sizes: {}, // 尺寸映射(依赖响应式)
renderData: [],
// 兼容多选
isCheckedAll: false, // 全选
isCheckedImn: false, // 控制半选样式
isHideAppend: false,
};
},
computed: {
tableColumns() {
return this.columns.map((column) => {
// 兼容多选
if (column.type === "selection") {
return {
title: () => {
return (
<a-checkbox
checked={this.isCheckedAll}
indeterminate={this.isCheckedImn}
onchange={() => this.onCheckAllRows(!this.isCheckedAll)}
></a-checkbox>
);
},
customRender: (text, row) => {
return (
<a-checkbox
checked={row.$v_checked}
onchange={() => this.onCheckRow(row, !row.$v_checked)}
></a-checkbox>
);
},
width: 60,
...column,
};
} else if (column.index) {
// 兼容索引
return {
customRender: (text, row, index) => {
const curIndex = this.start + index;
return typeof column.index === "function"
? column.index(curIndex)
: curIndex + 1;
},
...column,
};
}
return column;
});
},
// 计算出每个item(的key值)到滚动容器顶部的距离
offsetMap({ keyProp, itemSize, sizes, dataSource }) {
if (!this.dynamic) return {};
const res = {};
let total = 0;
for (let i = 0; i < dataSource.length; i++) {
const key = dataSource[i][keyProp];
res[key] = total;
const curSize = sizes[key];
const size = typeof curSize === "number" ? curSize : itemSize;
total += size;
}
return res;
},
},
methods: {
// 初始化数据
initData() {
// 是否是表格内部滚动
this.isInnerScroll = false;
this.scroller = this.getScroller();
this.setToTop();
// 首次需要执行2次handleScroll:因为第一次计算renderData时表格高度未确认导致计算不准确;第二次执行时,表格高度确认后,计算renderData是准确的
this.handleScroll();
this.$nextTick(() => {
this.handleScroll();
});
// 监听事件
this.onScroll = throttle(this.handleScroll, this.throttleTime);
this.scroller.addEventListener("scroll", this.onScroll);
window.addEventListener("resize", this.onScroll);
},
// 设置表格到滚动容器的距离
setToTop() {
if (this.isInnerScroll) {
this.toTop = 0;
} else {
this.toTop =
this.$el.getBoundingClientRect().top -
(this.scroller === window
? 0
: this.scroller.getBoundingClientRect().top) +
getScrollTop(this.scroller);
}
},
// 获取滚动元素
getScroller() {
let el;
if (this.scrollBox) {
if (this.scrollBox === "window" || this.scrollBox === window)
return window;
el = document.querySelector(this.scrollBox);
if (!el)
throw new Error(
` scrollBox prop: '${this.scrollBox}' is not a valid selector`
);
if (!isScroller(el))
console.warn(
`Warning! scrollBox prop: '${this.scrollBox}' is not a scroll element`
);
return el;
}
// 如果表格是固定高度,则获取表格内的滚动节点,否则获取父层滚动节点
if (this.$attrs.scroll && this.$attrs.scroll.y) {
this.isInnerScroll = true;
return this.$el.querySelector(".ant-table-body");
} else {
return getParentScroller(this.$el);
}
},
// 处理滚动事件
handleScroll() {
if (!this.virtualized) return;
// 更新当前尺寸(高度)
this.updateSizes();
// 计算renderData
this.calcRenderData();
// 计算位置
this.calcPosition();
},
// 更新尺寸(高度)
updateSizes() {
if (!this.dynamic) return;
let rows = [];
if (this.isTree) {
// 处理树形表格,筛选出一级树形结构
rows = this.$el.querySelectorAll(
".ant-table-body .ant-table-row-level-0"
);
} else {
rows = this.$el.querySelectorAll(
".ant-table-body .ant-table-tbody .ant-table-row"
);
}
Array.from(rows).forEach((row, index) => {
const item = this.renderData[index];
if (!item) return;
// 计算表格行的高度
let offsetHeight = row.offsetHeight;
// 表格行如果有扩展行,需要加上扩展内容的高度
const nextEl = row.nextSibling;
if (
nextEl &&
nextEl.classList &&
nextEl.classList.contains("ant-table-expanded-row")
) {
offsetHeight += row.nextSibling.offsetHeight;
}
// 表格行如果有子孙节点,需要加上子孙节点的高度
if (this.isTree) {
let next = row.nextSibling;
while (
next &&
next.tagName === "TR" &&
!next.classList.contains("ant-table-row-level-0")
) {
offsetHeight += next.offsetHeight;
next = next.nextSibling;
}
}
const key = item[this.keyProp];
if (this.sizes[key] !== offsetHeight) {
this.$set(this.sizes, key, offsetHeight);
row._offsetHeight = offsetHeight;
}
});
},
// 计算只在视图上渲染的数据
calcRenderData() {
const { scroller, buffer, dataSource: data } = this;
// 计算可视范围顶部、底部
const top = getScrollTop(scroller) - buffer - this.toTop;
const scrollerHeight = this.isInnerScroll
? this.$attrs.scroll.y
: getOffsetHeight(scroller);
const bottom =
getScrollTop(scroller) + scrollerHeight + buffer - this.toTop;
let start;
let end;
if (!this.dynamic) {
start = top <= 0 ? 0 : Math.floor(top / this.itemSize);
end = bottom <= 0 ? 0 : Math.ceil(bottom / this.itemSize);
} else {
// 二分法计算可视范围内的开始的第一个内容
let l = 0;
let r = data.length - 1;
let mid = 0;
while (l <= r) {
mid = Math.floor((l + r) / 2);
const midVal = this.getItemOffsetTop(mid);
if (midVal < top) {
const midNextVal = this.getItemOffsetTop(mid + 1);
if (midNextVal > top) break;
l = mid + 1;
} else {
r = mid - 1;
}
}
// 计算渲染内容的开始、结束索引
start = mid;
end = data.length - 1;
for (let i = start + 1; i < data.length; i++) {
const offsetTop = this.getItemOffsetTop(i);
if (offsetTop >= bottom) {
end = i;
break;
}
}
}
// 开始索引始终保持偶数,如果为奇数,则加1使其保持偶数【确保表格行的偶数数一致,不会导致斑马纹乱序显示】
if (start % 2) {
start = start - 1;
}
this.top = top;
this.bottom = bottom;
this.start = start;
this.end = end;
this.renderData = data.slice(start, end + 1);
this.$emit("change", this.renderData, this.start, this.end);
},
// 计算位置
calcPosition() {
const last = this.dataSource.length - 1;
// 计算内容总高度
const wrapHeight = this.getItemOffsetTop(last) + this.getItemSize(last);
// 计算当前滚动位置需要撑起的高度
const offsetTop = this.getItemOffsetTop(this.start);
// 设置dom位置
TableBodyClassNames.forEach((className) => {
const el = this.$el.querySelector(className);
if (!el) return;
// 创建wrapEl、innerEl
if (!el.wrapEl) {
const wrapEl = document.createElement("div");
const innerEl = document.createElement("div");
// 此处设置display为'inline-block',是让div宽度等于表格的宽度,修复x轴滚动时右边固定列没有阴影的bug
wrapEl.style.display = "inline-block";
innerEl.style.display = "inline-block";
wrapEl.appendChild(innerEl);
innerEl.appendChild(el.children[0]);
el.insertBefore(wrapEl, el.firstChild);
el.wrapEl = wrapEl;
el.innerEl = innerEl;
}
if (el.wrapEl) {
// 设置高度
el.wrapEl.style.height = wrapHeight + "px";
// 设置transform撑起高度
el.innerEl.style.transform = `translateY(${offsetTop}px)`;
// 设置paddingTop撑起高度
// el.innerEl.style.paddingTop = `${offsetTop}px`
}
});
},
// 获取某条数据offsetTop
getItemOffsetTop(index) {
if (!this.dynamic) {
return this.itemSize * index;
}
const item = this.dataSource[index];
if (item) {
return this.offsetMap[item[this.keyProp]] || 0;
}
return 0;
},
// 获取某条数据的尺寸
getItemSize(index) {
if (index <= -1) return 0;
const item = this.dataSource[index];
if (item) {
const key = item[this.keyProp];
return this.sizes[key] || this.itemSize;
}
return this.itemSize;
},
// 【外部调用】更新
update() {
this.setToTop();
this.handleScroll();
},
// 【外部调用】滚动到第几行
// (不太精确:滚动到第n行时,如果周围的表格行计算出真实高度后会更新高度,导致内容坍塌或撑起)
scrollTo(index, stop = false) {
const item = this.dataSource[index];
if (item && this.scroller) {
this.updateSizes();
this.calcRenderData();
this.$nextTick(() => {
const offsetTop = this.getItemOffsetTop(index);
scrollToY(this.scroller, offsetTop);
// 调用两次scrollTo,第一次滚动时,如果表格行初次渲染高度发生变化时,会导致滚动位置有偏差,此时需要第二次执行滚动,确保滚动位置无误
if (!stop) {
setTimeout(() => {
this.scrollTo(index, true);
}, 50);
}
});
}
},
// 渲染全部数据
renderAllData() {
this.renderData = this.dataSource;
this.$emit("change", this.dataSource, 0, this.dataSource.length - 1);
this.$nextTick(() => {
// 清除撑起的高度和位置
TableBodyClassNames.forEach((className) => {
const el = this.$el.querySelector(className);
if (!el) return;
if (el.wrapEl) {
// 设置高度
el.wrapEl.style.height = "auto";
// 设置transform撑起高度
el.innerEl.style.transform = `translateY(${0}px)`;
}
});
});
},
// 执行update方法更新虚拟滚动,且每次nextTick只能执行一次【在数据大于100条开启虚拟滚动时,由于监听了data、virtualized会连续触发两次update方法:第一次update时,(updateSize)计算尺寸里的渲染数据(renderData)与表格行的dom是一一对应,之后会改变渲染数据(renderData)的值;而第二次执行update时,renderData改变了,而表格行dom未改变,导致renderData与dom不一一对应,从而位置计算错误,最终渲染的数据对应不上。因此使用每次nextTick只能执行一次来避免bug发生】
doUpdate() {
if (this.hasDoUpdate) return; // nextTick内已经执行过一次就不执行
if (!this.scroller) return; // scroller不存在说明未初始化完成,不执行
// 启动虚拟滚动的瞬间,需要暂时隐藏el-table__append-wrapper里的内容,不然会导致滚动位置一直到append的内容处
this.isHideAppend = true;
this.update();
this.hasDoUpdate = true;
this.$nextTick(() => {
this.hasDoUpdate = false;
this.isHideAppend = false;
});
},
// 兼容多选:选择表格所有行
onCheckAllRows(val) {
val = this.isCheckedImn ? true : val;
this.dataSource.forEach((row) => {
if (row.$v_checked === val) return;
this.$set(row, "$v_checked", val);
this.$set(row, "$v_checkedOrder", val ? checkOrder++ : undefined);
});
this.isCheckedAll = val;
this.isCheckedImn = false;
this.emitSelectionChange();
// 取消全选,则重置checkOrder
if (val === false) checkOrder = 0;
},
// 兼容多选:选择表格某行
onCheckRow(row, val) {
if (row.$v_checked === val) return;
this.$set(row, "$v_checked", val);
this.$set(row, "$v_checkedOrder", val ? checkOrder++ : undefined);
const checkedLen = this.dataSource.filter(
(row) => row.$v_checked === true
).length;
if (checkedLen === 0) {
this.isCheckedAll = false;
this.isCheckedImn = false;
} else if (checkedLen === this.dataSource.length) {
this.isCheckedAll = true;
this.isCheckedImn = false;
} else {
this.isCheckedAll = false;
this.isCheckedImn = true;
}
this.emitSelectionChange();
},
// 多选:兼容表格selection-change事件
emitSelectionChange() {
const selection = this.dataSource
.filter((row) => row.$v_checked)
.sort((a, b) => a.$v_checkedOrder - b.$v_checkedOrder);
this.$emit("selection-change", selection);
},
// 多选:兼容表格toggleRowSelection方法
toggleRowSelection(row, selected) {
const val = typeof selected === "boolean" ? selected : !row.$v_checked;
this.onCheckRow(row, val);
},
// 多选:兼容表格clearSelection方法
clearSelection() {
this.isCheckedImn = false;
this.onCheckAllRows(false);
},
},
watch: {
dataSource() {
if (!this.virtualized) {
this.renderAllData();
} else {
this.doUpdate();
}
},
virtualized: {
immediate: true,
handler(val) {
if (!val) {
this.renderAllData();
} else {
this.doUpdate();
}
},
},
},
created() {
this.$nextTick(() => {
this.initData();
});
},
mounted() {
const appendEl = this.$refs.append;
this.$el.querySelector(".ant-table-body").appendChild(appendEl);
},
beforeDestroy() {
if (this.scroller) {
this.scroller.removeEventListener("scroll", this.onScroll);
window.removeEventListener("resize", this.onScroll);
}
},
};
</script>
<style lang="less"></style>
更多推荐
已为社区贡献2条内容
所有评论(0)