前言
大数据table渲染的时候会变得很卡顿,我们经常会改用虚拟表格来做优化。
主要原理是在可视区内在展示相关的数据,看不到的就不做渲染,根据滚动条移动的位置计算出可视区内元素集合。通过计算出的startIndex/endIndex截取真实数据列表拿出来做数据渲染。

虚拟滚动其他参考
别再抱怨后端一次性传给你 1w 条数据了,几行代码教会你虚拟滚动!
「前端进阶」高性能渲染十万条数据(虚拟列表)
我也研究了下公司用的vxe-table第三方组件,与大家分享~
vxe-table 组件解析
一.vxe-table 主体内容
vxe-table组件主要分为: X滚动条,Y滚动条,table左固定,table主体内容,table右固定 每个又内容块都是通过
table组装好拼起来的
主要更新数据的手段: 1.table y轴滚动条
vxe-table--scroll-y-handle元素绑定onScroll=$xeTable.triggerVirtualScrollYEvent做更新, 2.vxe-table--viewport-wrapper元素绑定滚轮wheel事件$xeTable.triggerBodyWheelEvent做更新, 3.table-bodyvxe-table--body-inner-wrapper元素绑定onScroll=$xeTable.triggerBodyScrollEvent事件做数据更新 这几个手段 => 做到vxe-table的y轴数据更新
1.主要结构
<div class="vxe-table">
<!-- a)主体内容 -->
<div class="vxe-table--render-wrapper">
<!-- a-1)renderBody部分 -->
<div class="vxe-table--layout-wrapper">
<div class="vxe-table--viewport-wrapper">
<!-- table主体内容 -->
<div class="vxe-table--main-wrapper">
<!-- 表头 -->
<TableHeaderComponent ref="refTableHeader" tableData tableColumn tableGroupColumn />
<!-- 表体 -->
<TableBodyComponent ref="refTableBody" tableData tableColumn />
<!-- 表尾 -->
<TableFooterComponent ref="refTableFooter" footerTableData tableColumn />
</div>
</div>
</div>
<!-- a-2)renderScrollY Y轴滚动条 -->
<div class="vxe-table--scroll-y-virtual">
<div class="vxe-table--scroll-y-wrapper">
<!-- renderScrollY Y轴滚动条-滚动事件 -->
<div class="vxe-table--scroll-y-handle" ref="refScrollYHandleElem" onScroll={triggerVirtualScrollYEvent}>
<!-- renderScrollY Y轴滚动条-长度 -->
<div class="vxe-table--scroll-y-space" ref="refScrollYSpaceElem"></div>
</div>
</div>
</div>
</div>
<!-- b)renderScrollX X轴滚动条 -->
<div class="vxe-table--scroll-y-virtual">
<div class="vxe-table--scroll-x-wrapper">
<!-- renderScrollY X轴滚动条-滚动事件 -->
<div class="vxe-table--scroll-x-handle" ref="refScrollXHandleElem" onScroll="triggerVirtualScrollXEvent">
<!-- renderScrollY X轴滚动条-长度 -->
<div class="vxe-table--scroll-x-space" ref="refScrollXSpaceElem"></div>
</div>
</div>
</div>
</div>
2.主要事件
由于table 的x轴滚动条
renderScrollX,y轴滚动条renderScrollY是额外新增的, 通过 滚动x/y轴滚动条触发table的滚动更新需要onScroll手动触发 (x轴$xeTable.triggerVirtualScrollXEvent, y轴$xeTable.triggerVirtualScrollYEvent) 做更新,vxe-table--viewport-wrapper元素绑定滚轮事件$xeTable.triggerBodyWheelEvent做更新
3.核心逻辑
<div class="vxe-table">
<!-- 主体内容 -->
<div class="vxe-table--render-wrapper">
<!-- 根据 props.scrollbarConfig 配置的 {x: {position: 'top'}} 控制横向x滚动条在table上(默认下) -->
{ scrollbarXToTop ? [renderBody(), renderScrollX()] : [renderScrollX(), renderBody()] }
</div>
</div>
<script lang="tsx">
// table壳 渲染
const renderBody = () => {
// 根据 props.scrollbarConfig 配置的 {y: {position: 'left'}} 控制纵向y滚动条在table上(默认右)
const scrollbarYToLeft = computeScrollbarYToLeft.value
return <div class="vxe-table--layout-wrapper">
{scrollbarYToLeft ? [
renderScrollY(),
renderViewport()
] : [
// 真实table
renderViewport(),
// 纵向滚动条
renderScrollY()
]}
</div>
}
// table 渲染
const renderViewport = () => {
const { showHeader, showFooter } = props
return <div ref="refTableViewportElem" class="vxe-table--viewport-wrapper">
{/*渲染table主体内容*/}
<div class="vxe-table--main-wrapper">
{/*表头*/}
{
showHeader ? <TableHeaderComponent ref="refTableHeader" tableData tableColumn tableGroupColumn /> : ''
}
{/*表体*/}
<TableBodyComponent ref="refTableBody" tableData tableColumn />
{/*表尾*/}
{
showHeader ? <TableFooterComponent ref="refTableFooter" footerTableData tableColumn /> : ''
}
</div>
{/*渲染table左/右固定*/}
<div class="vxe-table--fixed-wrapper">
{leftList.length && overflowX ? renderFixed('left') : ''}
{rightList.length && overflowX ? renderFixed('right') : ''}
</div>
</div>
}
// 渲染 左右侧 固定的 columns
const renderFixed = (fixedType: 'left' | 'right') => {
const isFixedLeft = fixedType === 'left'
return <div class={`vxe-table--fixed-${fixedType}-wrapper`} ref={isFixedLeft ? refLeftContainer : refRightContainer}>
{/*表头*/}
{
showHeader ? <TableHeaderComponent ref={isFixedLeft ? refTableLeftHeader : refTableRightHeader} fixedType tableData tableColumn tableGroupColumn fixedColumn /> : ''
}
{/*表体*/}
<TableBodyComponent ref={isFixedLeft ? refTableLeftBody : refTableRightBody} tableData tableColumn fixedColumn />
{/*表尾*/}
{
showHeader ? <TableFooterComponent ref={isFixedLeft ? refTableLeftFooter : refTableRightFooter} fixedType footerTableData tableColumn fixedColumn /> : ''
}
</div>
}
// x轴滚动条
const renderScrollX = () => {
return <div ref="refScrollXVirtualElem" class="vxe-table--scroll-x-virtual">
<div class="vxe-table--scroll-x-left-corner" ref="refScrollXLeftCornerElem"></div>
<div class="vxe-table--scroll-x-wrapper" ref="refScrollXWrapperElem">
<div class="vxe-table--scroll-x-handle" ref="refScrollXHandleElem" onScroll="triggerVirtualScrollXEvent">
<div class="vxe-table--scroll-x-space" ref="refScrollXSpaceElem"></div>
</div>
</div>
<div class="vxe-table--scroll-x-right-corner" ref="refScrollXRightCornerElem"></div>
</div>
}
// y轴滚动条
const renderScrollY = () => {
return <div ref="refScrollYVirtualElem" class="vxe-table--scroll-y-virtual">
<div class="vxe-table--scroll-y-top-corner" ref="refScrollYTopCornerElem"></div>
<div class="vxe-table--scroll-y-wrapper" ref="refScrollYWrapperElem">
<div class="vxe-table--scroll-y-handle" ref="refScrollYHandleElem" onScroll={triggerVirtualScrollYEvent}>
<div class="vxe-table--scroll-y-space" ref="refScrollYSpaceElem"></div>
</div>
</div>
<div class="vxe-table--scroll-y-bottom-corner" ref="refScrollYBottomCornerElem"></div>
</div>
}
// 触发纵向y虚拟滚动事件,通知外部组件滚动状态变化
const triggerVirtualScrollYEvent = (evnt) => {
const { elemStore, inWheelScroll, lastScrollTop, inHeaderScroll, inBodyScroll, inFooterScroll } = internalData
// 过滤掉 body滚动
if (inHeaderScroll || inBodyScroll || inFooterScroll) {
return
}
// 过滤掉 滚轮 滚动
if (inWheelScroll) {
return
}
// 虚拟y滚动
if (scrollYLoad) {
$xeTable.triggerScrollYEvent(evnt)
}
$xeTable.handleScrollEvent(evnt, isRollY, isRollX, scrollTop, scrollLeft, {
type: 'table',
fixed: ''
})
}
onMounted(() => {
const tableViewportEl = refTableViewportElem.value
if (tableViewportEl) {
// 绑定 滚轮事件 主要兼容 renderFixed('left' | 'right') table 渲染的部分
tableViewportEl.addEventListener('wheel', /*$xeTable.triggerBodyWheelEvent*/triggerBodyWheelEvent, { passive: false })
// $xeTable.triggerBodyWheelEvent
const triggerBodyWheelEvent = (evnt) => {
if (scrollYLoad) {
$xeTable.triggerScrollYEvent(evnt)
}
$xeTable.handleScrollEvent(evnt, isRollY, isRollX, currTopNum, bodyScrollElem.scrollLeft, {
type: 'table',
fixed: ''
})
}
}
})
const $xeTable = {
triggerScrollYEvent() {
// 纵向 Y 可视渲染处理
loadScrollYData()
const loadScrollYData = () => {
// 计算(存储在 scrollYStore) visibleStartIndex visibleEndIndex, startIndex endIndex
$xeTable.updateScrollYData()
}
},
// 计算 展示的 data数据
handleTableData(force?: boolean) {
const tableData = scrollYLoad ? fullList.slice(scrollYStore.startIndex, scrollYStore.endIndex) : fullList.slice(0)
reactData.tableData = tableData
},
// 更新y数据
updateScrollYData() {
$xeTable.handleTableData()
$xeTable.updateScrollYSpace()
},
// 更新纵向 Y 可视渲染上下剩余空间大小
updateScrollYSpace() {
const containerList = ['main', 'left', 'right']
if (bodyTableElem) {
bodyTableElem.style.transform = `translate(${reactData.scrollXLeft || 0}px, ${scrollYTop}px)`
}
containerList.forEach(name => {
const layoutList = ['header', 'body', 'footer']
layoutList.forEach(layout => {
const ySpaceElem = getRefElem(elemStore[`${name}-${layout}-ySpace`])
if (ySpaceElem) {
ySpaceElem.style.height = ySpaceHeight ? `${ySpaceHeight}px` : ''
}
})
})
},
// 处理滚动事件,计算新的 startIndex 和 endIndex,并触发重新渲染
handleScrollEvent (evnt, isRollY, isRollX, scrollTop, scrollLeft, params) {
// 更新 scrollTop
internalData.lastScrollTop = scrollTop
// 派发滚动条事件
dispatchEvent('scroll', evntParams, evnt)
}
}
</script>
详细源码参考: packages/table/src/table.ts
二.vxe-table TableBodyComponent(body.ts)
1.主要结构
<div class="vxe-table--body-wrapper">
<!-- 主体内容 -->
<div class="vxe-table--body-inner-wrapper" ref="refBodyScroll" onScroll={$xeTable.triggerBodyScrollEvent}>
<!-- a)Y轴占位高度 -->
<div class="vxe-body--y-space"></div>
<!-- b)table-body内容区 -->
<div class="vxe-table--body">
<!-- b-1)列宽设置 -->
<colgroup>
{/* columns配置渲染 */}
{renderColumnList.map((column, $columnIndex) => {
return <col style={`width: ${column.renderWidth}px`}></col>
})}
</colgroup>
<!-- b-2)内容 -->
<tbody>
<!-- tr>td渲染 renderRows(fixedType, isOptimizeMode, renderDataList, renderColumnList) -->
{renderColumnList.map(v => {
const tdVNs = tableColumn.map((column, $columnIndex) => {
// td
return <td class="vxe-body--column">
// 单元格内容
<div class="vxe-cell">
// td 真实渲染区域
<div class="vxe-cell--wrapper">
{column.renderCell(cellParams)}
</div>
</div>
</td>
})
return <tr class="vxe-body--row">
{tdVNs}
</tr>
})}
</tbody>
</div>
</div>
</div>
2.主要事件
table-body 内置了onScroll事件 触发
$xeTable.triggerBodyScrollEvent做数据更新
3.核心逻辑
<script lang="tsx">
// 渲染主体
const renderVN = () => {
const { fixedColumn, fixedType, tableColumn } = props
// table 的 $xeTable.triggerBodyScrollEvent
const triggerBodyScrollEvent = (evnt, fixedType) => {
// 标记body 滚动
internalData.inBodyScroll = true
if (scrollYLoad) {
$xeTable.triggerScrollYEvent(evnt)
}
$xeTable.handleScrollEvent(evnt, isRollY, isRollX, scrollTop, scrollLeft, {
type: 'body',
fixed: fixedType
})
}
const ons = {
onScroll(evnt){
// $xeTable.triggerBodyScrollEvent(evnt, fixedType)
triggerBodyScrollEvent(evnt, fixedType)
}
}
return <div class={['vxe-table--body-wrapper', fixedType ? `fixed-${fixedType}--wrapper` : 'body--wrapper']}>
<div class="vxe-table--body-inner-wrapper" >
{
fixedType ? '' : <div class="vxe-body--x-space"></div>
}
<div class="vxe-body--y-space"></div>
<table class="vxe-table--body">
{/*列宽*/}
<colgroup>
{/* renderColumnList: 即 调整后的tableColumn */}
{renderColumnList.map((column, $columnIndex) => {
return <col style={`width: ${column.renderWidth}px`}></col>
})}
</colgroup>
{/*内容*/}
<tbody ref="refBodyTBody">
{renderRows(fixedType, isOptimizeMode, renderDataList, renderColumnList)}
</tbody>
</table>
</div>
</div>
}
const renderRows = (fixedType: 'left' | 'right' | '', isOptimizeMode: boolean, tableData: any[], tableColumn: VxeTableDefines.ColumnInfo[]) => {
const rows: any[] = []
tableData.forEach((row, $rowIndex) => {
const tdVNs = tableColumn.map((column, $columnIndex) => {
// 渲染tds列
return renderTdColumn(seq, rowid, fixedType, isOptimizeMode, rowLevel, row, rowIndex, $rowIndex, _rowIndex, column, $columnIndex, tableColumn, tableData)
})
rows.push({
<tr class="vxe-body--row">{tdVNs}</tr>
})
}
return rows
}
// 渲染列
const renderTdColumn = (
seq: number | string,
rowid: string,
fixedType: 'left' | 'right' | '',
isOptimizeMode: boolean,
rowLevel: number,
row: any,
rowIndex: number,
$rowIndex: number,
_rowIndex: number,
column: VxeTableDefines.ColumnInfo,
$columnIndex: number,
columns: VxeTableDefines.ColumnInfo[],
items: any[]
) => {
// 判断是否是隐藏列
let fixedHiddenColumn = fixedType ? column.fixed !== fixedType : column.fixed && overflowX
const tdVNs: any[] = []
// 渲染单元格
// 若在该 column 不在body里面渲染(被安排到固定列渲染的部分 只作占位)
if (fixedHiddenColumn) {
tdVNs.push(<div class="vxe-cell"></div>)
} else {
tdVNs.push(<div class="vxe-cell">
<div class="vxe-cell--wrapper">
{column.renderCell(cellParams)}
</div>
</div>
)
}
// ... // 单元格宽度拖拽 // 单元格高度拖拽 其他相关逻辑...
return <td class="vxe-body--column">{tdVNs}</td>
}
</script>
参考源码: packages/table/src/body.ts
三.vxe-table TableHeaderComponent(header.ts)
1.主要结构
<div class="vxe-table--header-wrapper">
<!-- 主体内容 -->
<div class="vxe-table--header-inner-wrapper">
<!-- a)table-header内容区 -->
<table ref="refHeaderTable" class="vxe-table--header">
<!-- a-1)列宽设置 -->
<colgroup>
{/* columns配置渲染 */}
{renderColumnList.map((column, $columnIndex) => {
return <col style={`width: ${column.renderWidth}px`}></col>
})}
</colgroup>
<!-- a-2)头部 -->
<thead>
<!-- tr>thead渲染 renderHeads(isGroup, isOptimizeMode, renderHeaderList) -->
<tr class="vxe-header--row">
{renderColumnList.map(column => {
return <th class="vxe-header--column">
<div class="vxe-cell">
<div class="vxe-cell--wrapper">{column.renderHeader()}</div>
</div>
</th>
})}
</tr>
</thead>
</table>
</div>
</div>
2.核心逻辑
<script lang="tsx">
// 渲染主体
const renderVN = () => {
const { fixedType, fixedColumn, tableColumn } = props
let renderHeaderList = [renderColumnList]
return <div class={['vxe-table--header-wrapper', fixedType ? `fixed-${fixedType}--wrapper` : 'body--wrapper']}>
<div class="vxe-table--header-inner-wrapper" onScroll={event => triggerHeaderScrollEvent(event, fixedType)}>
{
fixedType ? '' : <div class="vxe-body--x-space"></div>
}
<table ref="refHeaderTable" class="vxe-table--header">
{/*列宽*/}
<colgroup>
{/* renderColumnList: 即 调整后的tableColumn */}
{renderColumnList.map((column, $columnIndex) => {
return <col style={`width: ${column.renderWidth}px`}></col>
})}
</colgroup>
{/*头部*/}
<thead ref="refHeaderTHead">
{renderHeads(isGroup, isOptimizeMode, renderHeaderList)}
</thead>
</table>
</div>
</div>
}
const renderHeads = (isGroup: boolean, isOptimizeMode: boolean, headerGroups: VxeTableDefines.ColumnInfo[][]) => {
const { fixedType } = props
return headerGroups.map((cols, $rowIndex) => {
return <tr class="vxe-header--row">
{/* @renderRows 逻辑 */}
{
cols.map((column, $columnIndex) => {
return <th class="vxe-header--column">
<div class="vxe-cell">
{/* 如果是隐藏列 只留占位不做渲染 */}
{
isVNPreEmptyStatus || (isOptimizeMode && fixedHiddenColumn) ? '' : <div class="vxe-cell--wrapper">{column.renderHeader()}</div>
}
</div>
</th>
})
}
</tr>
})
}
</script>
参考源码: packages/table/src/header.ts
四.vxe-table TableFooterComponent(footer.ts)
逻辑同上
TableHeaderComponent
1.主要结构
<div class="vxe-table--footer-wrapper">
<!-- 主体内容 -->
<div class="vxe-table--footer-inner-wrapper">
<!-- a)table-footer内容区 -->
<table ref="refFooterTable" class="vxe-table--footer">
<!-- a-1)列宽设置 -->
<colgroup>
{/* columns配置渲染 */}
{renderColumnList.map((column, $columnIndex) => {
return <col style={`width: ${column.renderWidth}px`}></col>
})}
</colgroup>
<!-- a-2)底部 -->
<tfoot>
<!-- tr>tfoot渲染 renderHeads(isOptimizeMode, renderHeaderList) -->
{footerTableData.map(v => {
return <tr class="vxe-footer--row">
{tableColumn.map((column, $columnIndex) => {
return <td class="vxe-footer--column">
// 单元格内容
<div class="vxe-cell">
<div class="vxe-cell--wrapper">
{column.renderFooter()}
</div>
</div>
</td>
})}
</tr>
})}
</tfoot>
</table>
</div>
</div>
2.核心逻辑
<script lang="tsx">
// 渲染主体
const renderVN = () => {
const { fixedType, fixedColumn, tableColumn } = props
return <div class={['vxe-table--footer-wrapper', fixedType ? `fixed-${fixedType}--wrapper` : 'body--wrapper']}>
<div class="vxe-table--footer-inner-wrapper" onScroll={event => triggerFooterScrollEvent(event, fixedType)}>
{
fixedType ? '' : <div class="vxe-body--x-space"></div>
}
<table ref="refFooterTable" class="vxe-table--footer">
{/*列宽*/}
<colgroup>
{/* renderColumnList: 即 调整后的tableColumn */}
{renderColumnList.map((column, $columnIndex) => {
return <col style={`width: ${column.renderWidth}px`}></col>
})}
</colgroup>
{/*底部*/}
<tfoot ref="refFooterTFoot">
{renderHeads(isOptimizeMode, renderHeaderList)}
</tfoot>
</table>
</div>
</div>
}
const renderHeads = (isOptimizeMode: boolean, renderColumnList: VxeTableDefines.ColumnInfo[]) => {
const { fixedType, footerTableData } = props
return footerTableData.map((row, $rowIndex) => {
return <tr class="vxe-footer--row">
{/* @renderRows 逻辑 */}
{
tableColumn.map((column, $columnIndex) => {
return <td class="vxe-footer--column">
<div class="vxe-cell">
{/* 如果是隐藏列 只留占位不做渲染 */}
{
isVNPreEmptyStatus ? '' : <div class="vxe-cell--wrapper">{column.renderFooter()}</div>
}
</div>
</td>
})
}
</tr>
})
}
</script>
参考源码: packages/table/src/footer.ts
五.vxe-table 源码
结语
感谢阅读,由于篇幅问题 后面会出一篇根据vxe-table的理解,写一个简易版的vxe-table再做分享