亲宝软件园·资讯

展开

antd 3.x Table组件如何快速实现虚拟列表详析

Yue栎廷 人气:0

1. 前言

随着互联网的发展,web展示的内容越来越丰富,也越来越无穷。我们在实际开发中难免会遇到长列表数据渲染,而又不适合分页的业务场景,如果浏览器直接渲染海量数据,会造成页面卡死,严重时导致浏览器资源耗尽,直接崩溃掉。这种情况用户与产品是无法接受的,浏览器性能与业务需求产生了对立,因此虚拟列表技术被提出,为这种尴尬的场面提供了一线生机。

2. 虚拟列表

虚拟列表其实是按需显示的一种实现,即只对可见区域进行渲染,对非可见区域中的数据不渲染或部分渲染的技术,从而达到极高的渲染性能。假设有10万条记录需要同时渲染,我们屏幕的可见区域的高度为500px,而列表项的高度为50px,则此时我们在屏幕中最多只能看到10个列表项,那么在渲染的时候,我们只需加载可视区的那10条即可,触发页面滚动时,实时替换当前应该展示在页面中的10条数据。

它的名词解释由一张图来诠释,如下:

触发滚动后,可视区域内的数据变化:

首先,准备dom结构:

<div className="wrapper" style={{
    position: "relative",
    overflow: "auto"
}}>
    <div className="placeholder-list" style={{ 
        height: `${visibleHeight}px` 
    }}></div>
    <div className="render-list" style={{ 
        postion: "absolute", 
        top: 0, 
        left: 0
    }}>...</div>
</div>

wrapper添加滚动事件实现逻辑:

scrollEvent(e){
    const startIdx = Math.floor(e.target.scrollTop / itemHeight);
    const endIdx = startIdx + visibleCount;
    setList(source.slice(startIdx, endIdx));
    // 设置偏移距离,保持数据在视图中
    const offset = startIdx * itemHeight;
    listRef.current.style.top = offset + "px";
}

我们发现,快速滚动时最下方会出现空白的现象,因为此时数据还没渲染成功。为了优化此空白,考虑多渲染2条数据作为缓冲区。因此visibelCount=Math.ceil(visibelHeight / itemHeight) + 2

代码完整示例:

import { useCallback, useEffect, useRef, useState } from "react";

const visibleHeight = 360;
const itemHeight = 50;
const visibleCount = Math.ceil(visibleHeight / itemHeight) + 2;
const totalCount = 100;

const source = Array.from(Array(totalCount), (item, index) => index);

export default function VirtualList() {
  const [list, setList] = useState(source);
  const listRef = useRef();

  const scrollEvent = useCallback((e) => {
    const startIdx = Math.floor(e.target.scrollTop / itemHeight);
    const endIdx = startIdx + visibleCount;
    setList(source.slice(startIdx, endIdx));
    const offset = startIdx * itemHeight;
    listRef.current.style.top = offset + "px";
  }, []);
  
  useEffect(() => {
    listRef.current = document.querySelector(".list");
  }, []);
  
  return (
    <div
      style={{
        backgroundColor: "#FFF",
        height: visibleHeight + 'px',
        textAlign: "center",
        overflow: "auto",
        position: "relative",
        overscrollBehavior: 'contain'
      }}
      onScroll={scrollEvent}
    >
      <div style={{ height: totalCount * itemHeight + 'px' }}></div>
      <div
        className="list"
        style={{
          position: "absolute",
          top: 0,
          left: 0,
          width: "100%",
          height: visibleHeight + 'px'
        }}
      >
        {list.map((item) => {
          return (
            <div
              key={item}
              style={{ height: itemHeight + 'px', borderBottom: "1px solid #eee" }}
            >
              {item}
            </div>
          );
        })}
      </div>
    </div>
  );
}

3. 虚拟table

终于来到了标题的内容,如何对antd table3.x进行虚拟表格的封装。其实和上述的代码差不多,只不过对于有的新手同学来讲,可能有点摸不着入口,所以有了本节内容。

目前在antd4.x版本table已经实现了开启虚拟列表的配置,拿来即用。针对3.x的版本自己实现了一个虚拟table,解决了业务上长列表渲染卡顿的问题。

注意:Table每项需要定高,因此columns属性中需要ellipsis:true保证数据只展示一行,溢出展示省略号。

根据上节内容介绍的虚拟列表思路,我们需要准备2个容器,一个内容容器,Table已经提供了,另一个占位容器没有提供,所以需要手动创建一个并放在合适的地方。通过开发者工具审查元素找到Table内提供的那个内容容器.ant-table-body table,获取其dom。父容器.ant-table-body,创建一个占位容器div,追加到父容器内。通过元素审查也知道Table tr高度为54px,即itemHeight=54

知道了类名,就可以获取到Table的dom为所欲为了。

useEffect(() => {
    const parentNode = document.querySelector('.ant-table-body');
    const table = document.querySelector('.ant-table-body table');
    // 用ref保持table方便在滚动事件中使用table dom
    tableRef.current = table;
    // 创建一个占位的div,高度等于所有数据高度,用来撑开容器展示滚动条
    const placeholderWrapper = document.createElement('div');
    placeholderWrapper.style.height = itemHeight * totalCount + 'px'
    parentNode.appendChild(placeholderWrapper);
    // 子绝父相口诀,为table设置定位,脱离文档流,把位置让给占位盒子
    parentNode.style.position = 'relative';
    table.style.position = 'absolute';
    table.style.top = 0;
    table.style.left = 0;
    // 添加滚动事件
    parentNode.addEventListener('scroll', scrollEvent)
    return () => {
      // 清理占位盒子
      parentNode.removeChild(placeholderWrapper);
      parentNode.removeEventListener('scroll', scrollEvent)
    }
  }, [scrollEvent]);

接下来实现滚动事件,和上节内容一致,保存范围索引到state中:

const scrollEvent = useCallback((e) => {
    const startIdx = Math.floor(e.target.scrollTop / itemHeight);
    const endIdx = startIdx + visibleCount;
    // 保存当前的范围索引,用来slice源数据给展示用
    setRange([startIdx, endIdx]);
    const offset = startIdx * itemHeight;
    tableRef.current.style.top = offset + "px";
  }, []);

根据范围索引,截取当前要展示的数据项

const [range, setRange] = useState([]);
// 这个renderList就是需要给Table组件的
const renderList = useMemo(() => {
    const [start, end] = range;
    return dataSource.slice(start, end)
  }, [range])

return <Table dataSource={renderList} />

全文示例代码Github地址

4.总结

本文只是实现了在固定每项列表高度的情况下的虚拟列表,现实很多情况是不定高的。这个比定高的复杂,不过原理也是一样的,多了一步需要计算渲染后的实际高度的步骤。后续会完善不定高的虚拟列表的实现。

本文的内容也是我在工作中遇到的情况,应该很多其他小伙伴也会遇到antd 3.x table的虚拟化的问题,希望能给小伙伴们一点思路。因此有了本文,也是自己一次关于输入与输出的记录与沉淀。

加载全部内容

相关教程
猜你喜欢
用户评论