vxe-table 虚拟滚动

vxe-table 虚拟滚动

vxe-table 官方的虚拟滚动问题蛮多的,开了虚拟滚动,行高就固定了,多余的内容就不显示,这能拿出来用?

还是来手动实现吧

思路

我们需要实现动态行高,肯定需要先渲染再去计算行高,所以我们需要使用两个表格组件,一个用来显示数据的,另一个用来计算高度
我们把显示数据的表格叫visibleList,计算高度的表格叫offscreenList

visibleList的表格高度单独赋值,这样就能显示滚动条
不过有个问题,vxe-table用到table标签,table标签的内容会均分高度,visibleList中的数据并不是完整数据,所以我们需要在首尾各添加一个div,用来填充高度

监听滚动事件我们使用vueuseuseEventListeneruseThrottleFn
这里又有另一个问题,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分别标记两个表格组件,用来查找文档,这里就叫visibleListoffscreenList
我们的数据源每条都有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标签写的,会更好操作,不过多多少少还是有些问题

posted @ 2024-06-14 16:44  .NET好耶  阅读(704)  评论(0编辑  收藏  举报