vxe-table 虚拟滚动
vxe-table 虚拟滚动
vxe-table 官方的虚拟滚动问题蛮多的,开了虚拟滚动,行高就固定了,多余的内容就不显示,这能拿出来用?
还是来手动实现吧
思路
我们需要实现动态行高,肯定需要先渲染再去计算行高,所以我们需要使用两个表格组件,一个用来显示数据的,另一个用来计算高度
我们把显示数据的表格叫visibleList
,计算高度的表格叫offscreenList
visibleList的表格高度单独赋值,这样就能显示滚动条
不过有个问题,vxe-table用到table标签,table标签的内容会均分高度,visibleList中的数据并不是完整数据,所以我们需要在首尾各添加一个div,用来填充高度
监听滚动事件我们使用vueuse
的useEventListener
和useThrottleFn
这里又有另一个问题,vxe-table内部就有滚动事件,因为我们的滚动事件会改变列表的数据,所以会触发vxe-tabl的滚动事件,然后这两个滚动事件会不断互相触发,所以我们需要一个变量记录最后滚动的高度,无变化就不做任何操作,直接返回
实现
设置样式
offscreenList需要隐藏,而且不能影响布局
visibility: hidden;
position: absolute;
top: 0px;
因为我们需要计算行高,防止数据没渲染就被计算,所以用样式穿透要去掉所有vxe-table默认的样式
vxe-table--body-wrapper
底下的列vxe-body--column
和单元格vxe-cell
都用这个
padding-top: 0px;
padding-bottom: 0px;
padding-left: 0px;
padding-right: 0px;
同时也要防止表格宽度没有初始化完成而影响计算结果,我们直接设置vxe-table--body
的宽度,因为vxe-table宽度没填满就渲染数据
overflow-y: hidden;
width: calc(100% - 1px) !important;
还有滚动条,这个也会影响表格宽度,所以直接把vxe-table--body-wrapper
的滚动条放出来
overflow-y: scroll;
还有一个会影响高度计算的,默认的空样式会导致表格渲染数据前高度不为零,vxe-table--empty-block
直接去掉
display: none;
变量
首先需要几个变量
list: [],//表格原始数据
cacheCount: 20,//缓存的数据量
tableHeight: 0,//表格高度,即可滚动的高度
cssList: [],//行样式列表,记录行高、top、bottom
lastScrollTop: -1,//最后滚动位置
visibleList
visibleList: [],//显示的表格数据
visibleHeight: 0,//可视区域高度
visibleRowClassName: "visible_row",//可视列表的行类名
visibleListStartIndex: 0,//可视数据的起始索引
visibleListEndIndex: 0,//可视数据的截止索引
offscreenList
offscreenList: [],//渲染的表格数据
offscreenRowClassName: "offscreen_row",//渲染的行类名
offscreenTableWidth: 0,//渲染的表格宽度,用于判断宽度是否初始化完成
再用ref
分别标记两个表格组件,用来查找文档,这里就叫visibleList
和offscreenList
了
我们的数据源每条都有index属性,这个是用来设置获取行高用的
之后就是代码部分,我这里就省略空值判断了
初始化可视区域高度
这个可视区域就是visibleList的vxe-table--body-wrapper
initVisibleListHeight:()=>
{
let isSuccess = false;
let tableRef = proxy.$refs.visibleList;
let tableElement: HTMLElement = tableRef.$el;
let tableScrollContainer = tableElement.querySelector(".vxe-table--body-wrapper");
let scrollHeight = scrollContainer.clientHeight;
if (scrollHeight > 0)
{
//赋值操作
isSuccess = true;
}
if(false == isSuccess)
{
proxy.$nextTick(() =>
{
initVisibleListHeight();
});
}
}
绑定滚动事件
这个滚动事件绑在visibleList的vxe-table--body-wrapper
上
initVisibleListScrollEvent:()=>
{
let isSuccess = false;
let tableRef = proxy.$refs.visibleList;
let tableElement: HTMLElement = tableRef.$el;
let tableScrollContainer = tableElement.querySelector(".vxe-table--body-wrapper");
let scrollHeight = scrollContainer.clientHeight;
if (scrollHeight > 0)
{
//初始化事件
let scrollHandlerThrottle = useThrottleFn(滚动事件处理函数, 300, true);
useEventListener(tableScrollContainer, 'scroll', scrollHandlerThrottle);
isSuccess = true;
}
if(false == isSuccess)
{
proxy.$nextTick(() =>
{
initVisibleListScrollEvent();
});
}
}
初始化表格宽度
这个表格宽度是offscreenList的vxe-table--body
宽度,宽度大于零再填充数据
initOffsccreenListTableWidth:()=>
{
let isSuccess = false;
let tableRef = proxy.$refs.offscreenList;
let tableElement: HTMLElement = tableRef.$el;
let tableBody = tableElement.querySelector(".vxe-table--body");
let tableBodyStyle = tableBody.style;
let tableBodyWidthStr: string = tableBodyStyle.width;
tableBodyWidthStr = tableBodyWidthStr.replace("px", "");
let tableBodyWidth = Number(tableBodyWidthStr);
if (tableBodyWidth > 0)
{
//赋值操作
isSuccess = true;
}
if(false == isSuccess)
{
proxy.$nextTick(() =>
{
initOffsccreenListTableWidth();
});
}
}
行类名设置
这块就分别给两个表格设置:row-class-name
函数,就是把每行数据的index加到类名后面,这个我就省略了
渲染数据计算高度
直接赋值给offscreenList
就行,cssList最后元素的bottom就是表格滚动条高度
let tableRef = proxy.$refs.offscreenList;
let tableElement: HTMLElement = tableRef.$el;
let index = 0;
let isAwait = false;
while (index < offscreenList.length)
{
isAwait = true;
let rowElement = tableElement.querySelector(`${类名}_${index}`);
let rowHeight = rowElement.clientHeight;
if(rowHeight > 0)
{
//top和bottom计算
//拿cssList最后一个元素即可
let rowCss={
height:rowHeight,
top:rowTop,
bottom:rowBottom,
};
cssList.push(rowCss);
index++;
}
if(true == isAwait)
{
await proxy.$nextTick(() => { console.log("nextTick") });
}
}
设置滚动条高度
每次计算完高度都要设置
setVisibleListTableHeight:(height:number)=>
{
let isSuccess = false;
let tableRef = proxy.$refs.visibleList;
let tableElement: HTMLElement = tableRef.$el;
let tableContainer = tableElement.querySelector(".vxe-table--body");
tableHeight = height;
tableContainer.setAttribute("height", height);
isSuccess = true;
if(false == isSuccess)
{
proxy.$nextTick(() =>
{
setVisibleListTableHeight();
});
}
}
滚动事件
这个滚动事件需要计算头尾div高度,并且生成各生成一条数据添加到visibleList里面
scrollEvent:(event: UIEvent)=>
{
let target = event.target;
let scrollTop = target.scrollTop;
if (scrollTop == lastScrollTop)
{
return;
}
lastScrollTop = scrollTop;
scrollEventHandler(scrollTop);
},
scrollEventHandler:(scrollTop:number)=>
{
let startIndex = 0;
let endIndex = 0;
//没有滚动条
if (scrollTop > 0 && tableHeight <= visibleHeight)
{
return;
}
let top = scrollTop;
let bottom = scrollTop + visibleHeight;
let startIndex = binarySearchIndex(top);
let endIndex = binarySearchIndex(bottom);
let oldStartIndex = visibleListStartIndex;
let oldEndIndex = visibleListEndIndex;
let newStartIndex = startIndex - cacheCount;
let newEndIndex = endIndex + cacheCount;
if ((0 == oldStartIndex && 0 == oldEndIndex) || newStartIndex != oldStartIndex || newEndIndex != oldEndIndex)
{
visibleListStartIndex = newStartIndex;
visibleListEndIndex = newEndIndex;
//从原始数据里提取数据
//添加首尾两个空项
let firstVisibleItem = visibleList[0];
let lastVisibleItem = visibleList[visibleList.length - 1];
let firstVisibleItemCss = cssList[firstVisibleItemIndex];
let headerHeight: number = firstVisibleItemCss.top;
let headerRow = {
index: "",
height: headerHeight,
};
let lastVisibleItemIndex = lastVisibleItem.index;
let lastVisibleItemCss = cssList[lastVisibleItemIndex];
let lastCssItem = cssList[cssEndIndex];
let lastCssItemBottom = lastCssItem.bottom;
let lastVisibleItemBottom: number = lastVisibleItemCss.bottom;
let footerHeight: number = lastCssItemBottom - lastVisibleItemBottom;
let footerRow = {
index: "",
height: footerHeight,
};
}
}
二分法查找索引
数据量肯定很大,所以用二分法效率更高
binarySearchIndex:(scrollTop: number)=>
{
let targerIndex = 0;
let list = cssList;
let startIndex = 0;
let endIndex = list.length - 1;
while (startIndex < endIndex)
{
let middleIndex = Math.floor((startIndex + endIndex + 1) / 2);
let middleValue = list[middleIndex].top;
if (middleValue <= scrollTop)
{
startIndex = middleIndex;
}
else
{
endIndex = middleIndex - 1;
}
}
targerIndex = startIndex;
return targerIndex;
}
省略的东西
- 每次计算完高度都要清空offscreenList
- 重置表格数据需要手动返回顶部,即调用滚动事件回到0的位置,
scrollEventHandler(0)
,lastScrollTop重置为-1
- 如果有分页操作,添加完新的数据需要调用滚动事件,滚动1px重新计算索引,
scrollEventHandler(lastScrollTop+1)
- 分页事件的实现,距离底部一定距离需要触发分页事件,这个在滚动事件里写,以及设置分页事件是否能够触发
- 尽量使用异步操作
已知的问题
- 列宽和列表宽不可变,真要去监听也行,但是性能问题很大,因为还要重新计算分页数据,如果真要做宽度变化,建议先绘制一条竖线跟随鼠标移动,松开鼠标再去重新计算
vxe-table 虚拟滚动 结束
vxe-table的问题还真是不少呢,其它组件库的表格组件不是用table标签写的,会更好操作,不过多多少少还是有些问题