前言
项目使用的公共表格组件是在ant-design-vue表格的基础上做了二次封装,表格组件在一次性加载几百条的数据时,会有明显的等待时间,基于这种现状开始分析影响表格加载速度的因素以及如何进行优化。
问题分析
分析各种因素对表格加载速度的影响需要统计表格加载时间,在js中先记录一个开始的时间戳,然后给表格赋值数据让表格开始渲染,接着用setTimeOut延迟一秒执行结束时间的记录,因为表格渲染阻塞js运行,所以只有等到表格渲染完,才会执行结束时间的记录,然后根据开始时间和结束时间计算表格的加载时间。
const startTime = new Date().getTime() // 开始时间
let endTime // 结束时间
dataSource = ... // 给表格赋值表格数据
setTimeout(() => {
endTime = new Date().getTime()
}, 1000) // 记录结束时间
通过控制变量,统计不同场景下表格的各功能对加载时间的影响(比较繁琐就省略不提),得出结论元素的数量是影响表格加载时间最主要的因素。保证功能的情况下减少元素数量只有通过虚拟滚动来实现。
实现虚拟滚动的构思
实现表格虚拟滚动的功能一般是获取当前可视区域的高度和已知的每行高度,计算出当前可展示的行数,根据行数和滚动的高度从数组数据中截取需要展示的内容。在滚动时根据滚动方向和滚动距离将已被遮挡的行定位到即将展示出来的区域,同时改变行的内容。
结合当前使用的ant-design-vue@1x表格组件,同时为了最小程度的改动原表格组件并保持原来的组件使用方式和使用习惯,通过另一种思路来实现虚拟滚动:
使用insertAdjacentElement在表格的tbody内部第一个子节点之前(也就是第一个tr之前)和表格的tbody内部最后一个子节点之后(也就是最后一个tr之后)分别插入一个新的tr,通过动态设置两个tr的高度来实现虚拟滚动,如下图:
假如要展示100行数据,实际页面可视区域能展示10行数据。初始时渲染第1到第12条数据共12行,顶部插入的tr高度为0,底部插入的tr高度为88行数据的高度,此时滚动条的效果也是100行数据的效果。当滚动条向下滑动页面向上滚动到第11行数据即将展示第12行数据时,将页面可视区域的12行tr赋值为第2到第13条数据,同时将顶部插入的tr高度增加一行数据高度,底部插入的tr减少一行数据的高度。
滚动条向上滑动页面向下滚动时,同样根据滚动的距离截取数据,同时减少顶部插入tr的高度和增加底部插入tr的高度。
关键代码分析
首先需要监听表格滚动事件:
const refTable = this.$refs.table
// 虚拟滚动-监听内容区域滚动
const body = refTable .getElementsByClassName('ant-table-body')[0]
if (body && this.virtualScroll) { // virtualScroll为true代表开启虚拟滚动,根据不同需求选择
body.addEventListener('scroll', this.virtualContentScroll)
}
滚动事件:
// 虚拟滚动-内容区域滚动回调事件
virtualContentScroll(e) {
// 上一次滚动行数
// contentScrollTop:内容区域滚动的高度;rowBasicHeight:每行的高度(每行高度需要统一)
const lastRows = Math.floor(this.contentScrollTop / this.rowBasicHeight)
// 当前滚动行数
const curRows = Math.floor(this.e.target.scrollTop / this.rowBasicHeight)
// 更新滚动高度
this.contentScrollTop = e.target.scrollTop
const {
topVirtualTds, // 虚拟滚动顶部插入td
bottomVirtualTds, // 虚拟滚动底部插入td
rowBasicHeight, // 每行基础高度。虚拟滚动需要每行高度一致
dataSource, // 表格数据
} = this
// 当前视图内可展示的行数
const contentViewRows = Math.ceil(this.contentHeight / rowBasicHeight)
if (curRows !== lastRows) {
topVirtualTds.forEach((t, index) => {
t.style.height = (curRows * rowBasicHeight) + 'px'
})
bottomVirtualTds.forEach(t => {
t.style.height = ((dataSource.length - curRows - contentViewRows) * rowBasicHeight) + 'px'
})
}
},
插入虚拟滚动的tr:
// 渲染结束后动态添加上下区域虚拟高度
this.$nextTick(() => {
this.topVirtualHeight = 0
this.bottomVirtualHeight = (dataLength - Math.ceil(this.contentHeight / rowBasicHeight)) * rowBasicHeight
const tBodys = this.$refs.antTable.$el.getElementsByClassName('ant-table-tbody')
const bodyArr = [...tBodys]
bodyArr.forEach(item => {
item.insertAdjacentElement('afterbegin', this.createEmptyRow(this.topVirtualHeight, 'topVirtualTds'))
item.insertAdjacentElement('beforeend', this.createEmptyRow(this.bottomVirtualHeight, 'bottomVirtualTds'))
})
})
// 虚拟滚动-创建虚拟row
createEmptyRow(h, key) {
const tr = document.createElement('tr')
const td = document.createElement('td')
td.style.height = `${h}px`
td.style.border = 'none'
this[key].push(td)
tr.appendChild(td)
return tr
},
总结
实现功能时很多地方比如获取tbody元素时都是数组,是因为ant-design-vue的表格组件的固定列是一个单独的table,如果左右两侧都有固定列,整个表格组件的内容区域实际是三个table拼起来的。
tbody内前后各插入一个tr,通过调节两个tr的高度来模拟滚动,同时也能让滚动条达到实际效果。