Virtualized Table 虚拟化表格 ^(beta)
在前端开发领域,表格一直都是一个高频出现的组件,尤其是在中后台和数据分析场景。但是,对于 Table V1来说,当一屏里超过 1000 条数据记录时,就会出现卡顿等性能问题,体验不是很好。
通过虚拟化表格组件,超大数据渲染将不再是一个头疼的问题。
该组件仍在测试中,生产环境使用可能有风险。若您发现了 bug 或问题,请于 GitHub 报告给我们以便修复。同时,有一些 API 并未在此文档中提及,因为部分还没有开发完全,因此我们不在此提及。
即使虚拟化的表格是高效的,但是当数据负载过大时,网络和内存容量也会成为您应用程序的瓶颈。因此请牢记,虚拟化表格永远不是最完美的解决方案,请考虑数据分页、过滤器等优化方案。
基础用法
让我们演示虚拟化表的性能,用10列和1 000行渲染一个基本示例。
export function TableV2Basic() { return ( <ElTableV2 columns={basicColumns} data={basicData} width={700} height={400} fixed /> );}
自动调整大小
如果不想手动向表格传递 width 和 height 属性,可以使用 AutoResizer 对表格组件进行封装。这会自动为你更新宽度和高度。
尝试调整您的浏览器大小来看看它是如何工作的。
由于 AutoResizer 组件的默认高度是 100%,所以请确保该组件的父元素拥有固定的高度值。或者,您可以通过将 style 属性传递到 AutoResizer 来定义它。
export function TableV2AutoResizer() { return ( <div style={resizerWrapperStyle}> <ElAutoResizer> {({ height, width }) => ( <ElTableV2 columns={autoResizerColumns} data={autoResizerData} width={width} height={height} fixed /> )} </ElAutoResizer> </div> );}
自定义单元格渲染器
当然,您可以根据您的需要呈现表格单元格。这是如何自定义您的单元格的简单例子。
export function TableV2CellTemplating() { return ( <ElTableV2 columns={cellTemplatingColumns} data={cellTemplatingData} width={700} height={400} fixed /> );}
带有选择的表格
使用自定义的单元格渲染来给表格组件添加选择的能力。
export function TableV2Selection() { const [data, setData] = useState<TableV2Row[]>(initialSelectionData); const allSelected = data.length > 0 && data.every((row) => row.checked); const containsChecked = data.some((row) => row.checked); const columns = useMemo<TableV2Column[]>( () => [ { key: "selection", width: 50, cellRenderer: ({ rowData }) => ( <ElCheckbox checked={Boolean(rowData.checked)} aria-label="选择本行" onCheckedChange={(checked) => setData((previous) => previous.map((row) => row.id === rowData.id ? { ...row, checked } : row, ), ) } /> ), headerCellRenderer: () => ( <ElCheckbox checked={allSelected} indeterminate={containsChecked && !allSelected} aria-label="全选" onCheckedChange={(checked) => setData((previous) => previous.map((row) => ({ ...row, checked })), ) } /> ), }, ...selectionBaseColumns, ], [allSelected, containsChecked], ); return ( <div style={resizerWrapperStyle}> <ElAutoResizer> {({ height, width }) => ( <ElTableV2 columns={columns} data={data} width={width} height={height} fixed /> )} </ElAutoResizer> </div> );}
可编辑单元格
类似上面添加筛选框的方法,我们可以用同样的方法实现可编辑单元格。
export function TableV2InlineEditing() { const [data, setData] = useState<TableV2Row[]>(initialEditingData); const columns = useMemo<TableV2Column[]>(() => { const updateRow = (rowId: unknown, patch: (row: TableV2Row) => TableV2Row) => setData((previous) => previous.map((row) => (row.id === rowId ? patch(row) : row)), ); const first: TableV2Column = { ...inlineEditingBaseColumns[0], title: "Editable Column", cellRenderer: ({ rowData, column }) => { const dataKey = column.dataKey as string; const exitEditMode = () => updateRow(rowData.id, (row) => ({ ...row, editing: false })); return rowData.editing ? ( <ElInput autoFocus value={String(rowData[dataKey] ?? "")} onChange={(value) => updateRow(rowData.id, (row) => ({ ...row, [dataKey]: value })) } onBlur={exitEditMode} onKeyDown={(event) => { if (event.key === "Enter") exitEditMode(); }} /> ) : ( <div style={{ padding: 4, border: "1px dotted transparent", cursor: "text" }} onClick={() => updateRow(rowData.id, (row) => ({ ...row, editing: true })) } > {String(rowData[dataKey] ?? "")} </div> ); }, }; return [first, ...inlineEditingBaseColumns.slice(1)]; }, []); return ( <div style={resizerWrapperStyle}> <ElAutoResizer> {({ height, width }) => ( <ElTableV2 columns={columns} data={data} width={width} height={height} fixed /> )} </ElAutoResizer> </div> );}
带状态的表格
可将表格内容 highlight 显示,方便区分「成功、信息、警告、危险」等内容。
要自定义行的外观,请使用 row-class-name 属性。举个例子,每10行会自动添加 bg-blue-200 类名,每5行会添加 bg-red-100 类名。
export function TableV2RowClass() { return ( <ElTableV2 columns={rowClassColumns} data={rowClassData} rowClass={rowClassGetter} cellProps={rowClassCellProps} width={700} height={400} /> );}
表格行的粘性布局
您可以简单地使用 fixed-data 属性来实现将某些行固定到表格的头部。
您可以根据滚动事件动态设置粘性行,如这个示例所示。
export function TableV2StickyRows() { const [stickyIndex, setStickyIndex] = useState(0); const fixedData = stickySourceData.slice(stickyIndex, stickyIndex + 1); const tableData = stickySourceData.slice(1); return ( <ElTableV2 columns={stickyColumns} data={tableData} fixedData={fixedData} rowClass={stickyRowClass} width={700} height={400} fixed onScroll={({ scrollTop }) => { const nextIndex = Math.floor(scrollTop / 250) * 5; setStickyIndex((previous) => previous === nextIndex ? previous : nextIndex, ); }} /> );}
固定列表格
如果您想要有列粘贴左侧或右侧的某种原因。 您可以通过向表中添加特殊属性来实现这一点。
您可以设置该行的 fixed 属性为 true (代表FixedDir.LEFT)、FixedDir.LEFT 或 FixedDir.RIGHT
export function TableV2FixedColumns() { const [data, setData] = useState<TableV2Row[]>(() => generateData(fixedColumns, 200), ); const [sortBy, setSortBy] = useState<TableV2SortBy>({ key: "column-0", order: TableV2SortOrder.ASC, }); return ( <ElTableV2 columns={fixedColumns} data={data} sortBy={sortBy} width={700} height={400} fixed onColumnSort={(nextSort) => { setData((previous) => [...previous].reverse()); setSortBy({ key: nextSort.key, order: nextSort.order }); }} /> );}
表头分组
正如这个示例,通过自定义表头渲染以将表头分组。
在这种情况下,我们使用了 JSX 功能,这个功能在playground上不被支持。您可以在本地环境或在线集成开发环境(如 codesandbox )中试用它们。
建议您使用 JSX 使用您的表格组件,因为它包含 VNode 操作。
export function TableV2GroupingHeader() { return ( <ElTableV2 fixed columns={groupingColumns} data={groupingData} headerHeight={[50, 40, 50]} headerClass={groupingHeaderClass} width={700} height={400} headerRenderer={renderGroupedHeader} /> );}
过滤器
虚拟表格提供自定义页眉渲染器以创建自定义标题。然后我们可以利用这些来渲染过滤器。
export function TableV2Filter() { const [shouldFilter, setShouldFilter] = useState(false); const [data, setData] = useState<TableV2Row[]>(() => generateData(filterBaseColumns, 200), ); const popoverRef = useRef<PopoverExpose>(null); const onFilter = () => { popoverRef.current?.hide(); setData( shouldFilter ? generateData(filterBaseColumns, 100, "filtered-") : generateData(filterBaseColumns, 200), ); }; const onReset = () => { setShouldFilter(false); popoverRef.current?.hide(); setData(generateData(filterBaseColumns, 200)); }; const columns: TableV2Column[] = filterColumns.map( (column, columnIndex) => columnIndex === 0 ? { ...column, headerCellRenderer: () => ( <div style={{ display: "flex", alignItems: "center", justifyContent: "center", }} > <span style={{ marginRight: 8, fontSize: 12 }}> {column.title} </span> <ElPopover ref={popoverRef} trigger="click" width={200} content={ <div> <div style={{ padding: "4px 0" }}> <ElCheckbox checked={shouldFilter} onCheckedChange={setShouldFilter} > Filter Text </ElCheckbox> </div> <div style={{ display: "flex", justifyContent: "space-between", borderTop: "1px solid var(--el-border-color-lighter)", marginTop: 12, paddingTop: 8, }} > <ElButton text onClick={onFilter}>Confirm</ElButton> <ElButton text onClick={onReset}>Reset</ElButton> </div> </div> } > <button type="button" aria-label={`筛选 ${column.title}`} style={{ display: "flex", cursor: "pointer", padding: 0, margin: 0, background: "none", appearance: "none", border: "none", }} > <ElIcon size={14}> <Filter /> </ElIcon> </button> </ElPopover> </div> ), } : column, ); return ( <ElTableV2 fixed columns={columns} data={data} width={700} height={400} /> );}
可排序表格
您可以使用排序状态来对表格进行排序。
export function TableV2Sort() { const [data, setData] = useState<TableV2Row[]>(() => generateData(sortColumns, 200), ); const [sortBy, setSortBy] = useState<TableV2SortBy>({ key: "column-0", order: TableV2SortOrder.ASC, }); return ( <ElTableV2 columns={sortColumns} data={data} sortBy={sortBy} width={700} height={400} fixed onColumnSort={(nextSort) => { setData((previous) => [...previous].reverse()); setSortBy({ key: nextSort.key, order: nextSort.order }); }} /> );}
受控的排序
您可以在需要时定义多个可排序的列。请记住,当您在定义了多个可排序的列时, UI 可能会显得有些奇怪,因为用户不知道哪一列被排序。
export function TableV2ControlledSort() { const [data, setData] = useState<TableV2Row[]>(() => generateData(controlledSortColumns, 200), ); const [sortState, setSortState] = useState<TableV2SortState>({ "column-0": TableV2SortOrder.DESC, "column-1": TableV2SortOrder.ASC, }); return ( <ElTableV2 columns={controlledSortColumns} data={data} sortState={sortState} width={700} height={400} fixed onColumnSort={({ key, order }) => { setSortState((previous) => ({ ...previous, [key]: order })); setData((previous) => [...previous].reverse()); }} /> );}
高亮显示鼠标悬停单元格
当处理一个大的列表时,很容易丢失当前行的轨迹和您正在访问的一列。在这种情况下,使用这个功能可能很有帮助。
export function TableV2CrossHovering() { const [hoveredColumn, setHoveredColumn] = useState<number | null>(null); const cellProps = ({ columnIndex }: { columnIndex: number }) => ({ "data-key": `hovering-col-${columnIndex}`, style: hoveredColumn === columnIndex ? { backgroundColor: rowHoverBackground } : undefined, onMouseEnter: () => setHoveredColumn(columnIndex), onMouseLeave: () => setHoveredColumn(null), }); return ( <div style={resizerWrapperStyle}> <ElAutoResizer> {({ height, width }) => ( <ElTableV2 columns={crossHoveringColumns} cellProps={cellProps} data={crossHoveringData} width={width} height={height} /> )} </ElAutoResizer> </div> );}
横跨列
虚拟化表格没有使用内置的 table 元素,故 colspan 和 rowspan 与 TableV1 比较略有不同。然而,通过定制的行渲染器,这些功能仍然可以实现。在本节中,我们将演示如何实现这一点。
export function TableV2Colspan() { const renderRow = ({ cells, columns, rowIndex }: TableV2RowSlotProps) => applyColSpan(cells, columns, rowIndex); return ( <ElTableV2 fixed columns={spanColumns} data={spanData} width={700} height={400} rowRenderer={renderRow} /> );}
纵跨行
既然我们已经提到了 Colspan,那跨行(row span)也是没有问题的。它与colspan略有不同,但是基本原理是一样的。
export function TableV2Rowspan() { const renderRow = ({ cells, columns, rowIndex }: TableV2RowSlotProps) => { const next = [...cells]; const rowSpan = ( columns[rowSpanIndex] as { rowSpan?: (params: { rowIndex: number }) => number; } ).rowSpan?.({ rowIndex }); if (rowSpan && rowSpan > 1) { next[rowSpanIndex] = cloneCellStyle(next[rowSpanIndex], { backgroundColor: "var(--el-color-primary-light-3)", height: rowSpan * 50 - 1, alignSelf: "flex-start", zIndex: 1, }); } return next; }; return ( <ElTableV2 fixed columns={spanColumns} data={spanData} width={700} height={400} rowRenderer={renderRow} /> );}
同时跨行和跨列
我们当然可以同时使用横跨列与纵跨行来满足您的业务需求!
export function TableV2Spans() { const renderRow = ({ cells, columns, rowIndex }: TableV2RowSlotProps) => { const next = applyColSpan(cells, columns, rowIndex); const rowSpan = ( columns[rowSpanIndex] as { rowSpan?: (params: { rowIndex: number }) => number; } ).rowSpan?.({ rowIndex }); if (rowSpan && rowSpan > 1) { next[rowSpanIndex] = cloneCellStyle(next[rowSpanIndex], { backgroundColor: "var(--el-color-danger-light-3)", height: rowSpan * 50, alignSelf: "flex-start", zIndex: 1, }); } else { next[rowSpanIndex] = ( <div key={`span-empty-${rowIndex}`} style={{ width: cellElementStyle(next[rowSpanIndex]).width }} /> ); } return next; }; return ( <ElTableV2 fixed columns={spanColumns} data={spanData} width={700} height={400} rowRenderer={renderRow} /> );}
树形数据
虚拟表也可以在树状结构中呈现数据。点击箭头图标,你可以展开或折叠树节点。
export function TableV2TreeData() { const [expandedRowKeys, setExpandedRowKeys] = useState<TableV2KeyType[]>([]); return ( <ElTableV2 columns={treeColumns} data={treeData} expandedRowKeys={expandedRowKeys} expandColumnKey="column-0" width={700} height={400} fixed onExpandedRowsChange={setExpandedRowKeys} /> );}
动态高度行
虚拟表能够呈现具有动态高度的行数。如果您正在处理数据并不确定内容大小, 此功能对于调整到内容高度的渲染行是理想的。要启用此功能,请传递 estimated-row-height 属性。估计高度越接近实际内容,渲染体验就越顺。
每行高度在渲染过程中动态测量。因此,如果您试图显示大量数据, UI 可能会 抖动。
Tom
Corrupti doloremque a quos vero delectus consequatur.
Tom
Corrupti doloremque a quos vero delectus consequatur.
Tom
Corrupti doloremque a quos vero delectus consequatur.
Tom
Corrupti doloremque a quos vero delectus consequatur.
Tom
Corrupti doloremque a quos vero delectus consequatur.
Tom
Corrupti doloremque a quos vero delectus consequatur.
Tom
Quaerat ipsam necessitatibus eum quibusdam est id voluptatem cumque mollitia.
export function TableV2DynamicHeight() { const [data, setData] = useState<TableV2Row[]>(dynamicHeightData); const [sortBy, setSortBy] = useState<TableV2SortBy>({ key: "name", order: TableV2SortOrder.ASC, }); return ( <ElTableV2 columns={dynamicHeightColumns} data={data} sortBy={sortBy} estimatedRowHeight={40} width={700} height={400} fixed onColumnSort={({ key, order }) => { const factor = order === TableV2SortOrder.ASC ? 1 : -1; const dataKey = String(key); setData((previous) => [...previous].sort((left, right) => String(left[dataKey]) > String(right[dataKey]) ? factor : -factor, ), ); setSortBy({ key, order }); }} /> );}
可展开的附加信息
使用动态高度渲染,您也可以在表格中显示详细的视图。
export function TableV2DetailedView() { return ( <div className={tableV2Styles.detailedView}> <ElTableV2 columns={detailColumns} data={detailData} estimatedRowHeight={50} expandColumnKey={String(detailColumns[0].key)} width={700} height={400} rowRenderer={renderDetailRow} /> </div> );}
自定义页脚
自定义表格 footer, 通常用来展示一些汇总数据和信息。
export function TableV2Footer() { return ( <ElTableV2 columns={footerColumns} data={footerData} rowHeight={40} width={700} height={400} footerHeight={50} footer={footerContent} fixed /> );}
自定义空元素渲染器
渲染自定义的空元素。
export function TableV2Empty() { return ( <ElTableV2 columns={emptyColumns} data={[]} rowHeight={40} width={700} height={400} footerHeight={50} empty={ <div style={{ display: "flex", alignItems: "center", justifyContent: "center", height: "100%", }} > <ElEmpty /> </div> } /> );}
浮动遮罩层
当您想要显示加载指示器之类的浮动元素,可以通过渲染一个浮动在表格之上的遮罩层来实现。
export function TableV2Overlay() { return ( <ElTableV2 columns={overlayColumns} data={overlayData} rowHeight={40} width={700} height={400} overlay={ <div className="el-loading-mask" style={{ display: "flex", alignItems: "center", justifyContent: "center", position: "absolute", inset: 0, }} > <ElIcon className="is-loading" color="var(--el-color-primary)" size={26}> <Loading /> </ElIcon> </div> } /> );}
手动滚动
使用 Table V2 暴露的方法可以进行手动或编程式的滚动到指定的偏移量或者行。
scrollToRow 的第二个参数代表滚动策略,计算了要滚动的位置,其默认值是 auto。如果你想要滚动到某个特定位置,你可以自己定义战略。可用的选项是 "auto" | "center" | "end" | "start" | "smart"
smart 和auto 之间的区别是, auto 是 smart 滚动策略的子集。
export function TableV2ManualScroll() { const tableRef = useRef<TableV2Expose>(null); const [scrollDelta, setScrollDelta] = useState("200"); const [scrollRows, setScrollRows] = useState("10"); return ( <> <div style={{ display: "flex", alignItems: "center", marginBottom: 16 }}> <ElFormItem label="Scroll pixels" style={{ marginRight: 16 }}> <ElInput value={scrollDelta} onChange={setScrollDelta} /> </ElFormItem> <ElFormItem label="Scroll rows"> <ElInput value={scrollRows} onChange={setScrollRows} /> </ElFormItem> </div> <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 16 }}> <ElButton onClick={() => tableRef.current?.scrollToTop(Number(scrollDelta) || 0)} > Scroll by pixels </ElButton> <ElButton onClick={() => tableRef.current?.scrollToRow(Number(scrollRows) || 0)} > Scroll by rows </ElButton> </div> <div style={resizerWrapperStyle}> <ElAutoResizer> {({ height, width }) => ( <ElTableV2 ref={tableRef} columns={manualColumns} data={manualData} width={width} height={height} fixed /> )} </ElAutoResizer> </div> </> );}
TableV2 API
TableV2 Attributes
| 属性名 | 说明 | 类型 | 默认值 |
|---|
| cache | 额外预渲染的行数,用于提升滚动性能 | number | 2 |
| estimated-row-height | 动态行高的估算值;提供后启用行高测量 | number | — |
| header-class | 表头行的自定义类名 | string / Function | — |
| header-props | 透传给表头的属性 | object / Function | — |
| header-cell-props | 透传给表头单元格的属性 | object / Function | — |
| header-height | 表头高度;传数组时按数组长度渲染多行表头 | number / ^[number[]] | 50 |
| footer-height | 底部高度,参与表格总高度计算 | number | 0 |
| row-class | 行自定义类名 | string / Function | — |
| row-key | 行唯一标识字段 | string / number / symbol | id |
| row-props | 透传给行的属性 | object / Function | — |
| row-height | 行高(固定行高模式) | number | 50 |
| row-event-handlers | 行事件集合,见下方 RowEventHandlers | object | — |
| cell-props | 透传给每个单元格的属性 | object / Function | — |
| columns | 列定义数组 | array | — |
| data | 表格数据 | array | [] |
| data-getter | 自定义单元格取值方法 | Function | — |
| fixed-data | 固定在表头下方、主内容上方的行数据 | array | — |
| expand-column-key | 展开图标所在的列 key | string | — |
| expanded-row-keys | 展开行的 key 数组(受控) | array | — |
| default-expanded-row-keys | 初始展开行的 key 数组(非响应式) | array | — |
| class-name / style | 根元素类名与样式 | string / object | — |
| fixed | 列宽固定还是弹性分配 | boolean | false |
| width required | 表格宽度 | number | — |
| height required | 表格高度 | number | — |
| max-height | 表格最大高度 | number | — |
| indent-size | 树形层级的水平缩进 | number | 12 |
| icon-size | 展开图标尺寸 | number | 12 |
| h-scrollbar-size | 横向滚动条尺寸 | number | 6 |
| v-scrollbar-size | 纵向滚动条尺寸 | number | 6 |
| scrollbar-always-on | 是否始终显示滚动条 | boolean | false |
| sort-by | 单列排序状态 | object | {} |
| sort-state | 多列排序状态 | object | undefined |
TableV2 Events
| 事件名 | 说明 | 参数 |
|---|
| onColumnSort | 列排序变化时触发 | ({ column, key, order }) |
| onExpandedRowsChange | 展开行变化时触发 | (expandedRowKeys) |
| onEndReached | 滚动到末尾时触发,参数为剩余距离 | (remainDistance) |
| onScroll | 滚动时触发 | ({ scrollLeft, scrollTop, ... }) |
| onRowsRendered | 行渲染完成时触发 | ({ rowCacheStart, rowCacheEnd, rowVisibleStart, rowVisibleEnd }) |
| onRowExpand | 点击展开箭头时触发 | ({ expanded, rowData, rowIndex, rowKey }) |
TableV2 Slots
React 版把作用域插槽映射为 render prop,具名插槽映射为 ReactNode 属性。
| 名称 | 说明 | React 映射 |
|---|
| cell | 自定义单元格内容 | cellRenderer({ cellData, column, columns, columnIndex, rowData, rowIndex }) |
| header | 自定义整行表头 | headerRenderer({ cells, columns, headerIndex }) |
| header-cell | 自定义表头单元格 | headerCellRenderer(params) |
| row | 自定义整行 | rowRenderer({ cells, columns, depth, style, rowData, rowIndex, isScrolling }) |
| footer | 表格底部 | footer 属性(ReactNode) |
| empty | 数据为空时的内容 | empty 属性(ReactNode) |
| overlay | 覆盖在表格上的内容 | overlay 属性(ReactNode) |
TableV2 Exposes
通过 ref 获取实例方法。
| 方法 | 说明 | 参数 |
|---|
| scrollTo | 滚动到指定位置 | ({ scrollLeft?, scrollTop? }) |
| scrollToLeft | 滚动到指定横向位置 | (scrollLeft) |
| scrollToTop | 滚动到指定纵向位置 | (scrollTop) |
| scrollToRow | 滚动到指定行,可指定策略 | (row, strategy?) |
Column Attributes
| 属性名 | 说明 | 类型 | 默认值 |
|---|
| align | 单元格内容对齐方式 | string | left |
| class | 列的自定义类名或 getter | string / Function | — |
| key | 列唯一标识 | string / number / symbol | — |
| dataKey | 数据字段名,支持 a.b[0].c | string / number / symbol | — |
| fixed | 固定方向 | boolean / string | false |
| flexGrow | flex-grow;仅非 fixed 表生效 | number | 0 |
| flexShrink | flex-shrink;仅非 fixed 表生效 | number | 1 |
| headerClass | 表头单元格类名 | string / Function | — |
| hidden | 是否隐藏该列 | boolean | — |
| style | 单元格样式,与网格样式合并 | object | — |
| sortable | 是否可排序 | boolean | — |
| title | 表头默认文本 | string | — |
| maxWidth / minWidth | 列最大/最小宽度 | number | — |
| width required | 列宽 | number | — |
| cellRenderer | 自定义单元格渲染函数 | Function | — |
| headerCellRenderer | 自定义表头单元格渲染函数 | Function | — |
| dataGetter | 自定义取值函数 | Function | — |