From 44fd457157a3ce229743c74d1f686cfd4c359ef3 Mon Sep 17 00:00:00 2001 From: XieZongChen <46394163+amadeus711@users.noreply.github.com> Date: Tue, 26 Oct 2021 13:15:36 -0500 Subject: [PATCH] feat(data-table): refactor virtual-scroll (#1295) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor(tabs): on-before-leave prop * fix(tabs): typo * fix(upload): file can't be removed when file count limit is reached, closes #1401 * 2.19.11 * test(dialog): Update dialog component test (#1404) * test(data-table): update test (#1411) * feat(data-table): restructure virtual-scroll * feat(data-table): optimization * feat: optimization * feat(data-table): optimization expanded * feat: optimization * feat: optimization * feat: optimization * feat: optimization * feat: optimization * feat: add change log * Update src/data-table/src/TableParts/Body.tsx Co-authored-by: 张乐聪 Co-authored-by: songjianet <1778651752@qq.com> Co-authored-by: 07akioni <07akioni2@gmail.com> --- CHANGELOG.en-US.md | 1 + CHANGELOG.zh-CN.md | 1 + src/data-table/src/TableParts/Body.tsx | 476 +++++++++++++------------ 3 files changed, 259 insertions(+), 219 deletions(-) diff --git a/CHANGELOG.en-US.md b/CHANGELOG.en-US.md index 1917037be1d..2df4f4d903f 100644 --- a/CHANGELOG.en-US.md +++ b/CHANGELOG.en-US.md @@ -11,6 +11,7 @@ ### Feats +- `n-data-table` optimize the logic of underlying rendering and improve component performance. - `n-date-picker`'s `shortcuts` prop supports functional value. ## 2.19.11 (2021-10-21) diff --git a/CHANGELOG.zh-CN.md b/CHANGELOG.zh-CN.md index e19255e3f94..d8b0b57a846 100644 --- a/CHANGELOG.zh-CN.md +++ b/CHANGELOG.zh-CN.md @@ -11,6 +11,7 @@ ### Feats +- `n-data-table` 优化底层渲染的逻辑,提升组件性能 - `n-date-picker` 的 `shortcuts` 属性支持传入回调函数 ## 2.19.11 (2021-10-21) diff --git a/src/data-table/src/TableParts/Body.tsx b/src/data-table/src/TableParts/Body.tsx index 6a84edbd438..ab6e609ed2f 100644 --- a/src/data-table/src/TableParts/Body.tsx +++ b/src/data-table/src/TableParts/Body.tsx @@ -8,7 +8,8 @@ import { watchEffect, onUnmounted, PropType, - CSSProperties + CSSProperties, + computed } from 'vue' import { pxfy, repeat } from 'seemly' import { VirtualList, VirtualListInst } from 'vueuc' @@ -37,12 +38,17 @@ type RowRenderInfo = disabled: boolean } | TmNode + | { + isExpandedRow: true + tmNode: TmNode + key: RowKey + } -function flatten (rows: TmNode[], expandedRowKeys: RowKey[]): TmNode[] { +function flatten (rows: TmNode[], expandedRowKeys: Set): TmNode[] { const fRows: TmNode[] = [] function traverse (rs: TmNode[]): void { rs.forEach((r) => { - if (r.children && expandedRowKeys.includes(r.key)) { + if (r.children && expandedRowKeys.has(r.key)) { fRows.push(r) traverse(r.children) } else { @@ -138,6 +144,9 @@ export default defineComponent({ const scrollbarInstRef = ref(null) const virtualListRef = ref(null) let lastSelectedKey: string | number = '' + const mergedExpandedRowKeySetRef = computed(() => { + return new Set(mergedExpandedRowKeysRef.value) + }) function handleCheckboxUpdateChecked ( tmNode: { key: RowKey }, checked: boolean, @@ -286,7 +295,7 @@ export default defineComponent({ currentPage: mergedCurrentPageRef, rowClassName: rowClassNameRef, renderExpand: renderExpandRef, - mergedExpandedRowKeys: mergedExpandedRowKeysRef, + mergedExpandedRowKeySet: mergedExpandedRowKeySetRef, hoverKey: hoverKeyRef, mergedSortState: mergedSortStateRef, virtualScroll: virtualScrollRef, @@ -354,8 +363,9 @@ export default defineComponent({ > {{ default: () => { + // coordinate to pass if there are cells that cross row & col const cordToPass: Record = {} - // coord to related hover keys + // coordinate to related hover keys const cordKey: Record> = {} const { cols, @@ -366,7 +376,7 @@ export default defineComponent({ currentPage, rowClassName, mergedSortState, - mergedExpandedRowKeys, + mergedExpandedRowKeySet, componentId, showHeader, hasChildren, @@ -390,7 +400,7 @@ export default defineComponent({ // if there is children in data, we should expand mergedData first const mergedPaginationData = hasChildren - ? flatten(paginatedData, mergedExpandedRowKeys) + ? flatten(paginatedData, mergedExpandedRowKeySet) : paginatedData if (summary) { @@ -420,170 +430,43 @@ export default defineComponent({ mergedData = mergedPaginationData } - const { length: rowCount } = mergedData - const indentStyle = hasChildren ? { width: pxfy(this.indent) } : undefined - const rows: VNode[] = [] - mergedData.forEach((rowInfo, rowIndex) => { - const { rawNode: rowData, key: rowKey } = rowInfo - const isSummary = 'summary' in rowInfo - const expanded = mergedExpandedRowKeys.includes(rowKey) - const showExpandContent = renderExpand && expanded - const colNodes = cols.map((col, colIndex) => { - if (rowIndex in cordToPass) { - const cordOfRowToPass = cordToPass[rowIndex] - const indexInCordOfRowToPass = - cordOfRowToPass.indexOf(colIndex) - if (~indexInCordOfRowToPass) { - cordOfRowToPass.splice(indexInCordOfRowToPass, 1) - return null - } - } - const { column } = col - const colKey = getColKey(col) - // If there is no rowSpan - // virtual list should have a fast path - const { rowSpan, colSpan } = column - const mergedColSpan = isSummary - ? rowInfo.rawNode[colKey]?.colSpan || 1 // optional for #1276 - : colSpan - ? colSpan(rowData, rowIndex) - : 1 - const mergedRowSpan = isSummary - ? rowInfo.rawNode[colKey]?.rowSpan || 1 // optional for #1276 - : rowSpan - ? rowSpan(rowData, rowIndex) - : 1 - const isLastCol = colIndex + mergedColSpan === colCount - const isLastRow = rowIndex + mergedRowSpan === rowCount - const isCrossRowTd = mergedRowSpan > 1 - if (isCrossRowTd) { - cordKey[rowIndex] = { - [colIndex]: [] - } - } - if (mergedColSpan > 1 || isCrossRowTd) { - for (let i = rowIndex; i < rowIndex + mergedRowSpan; ++i) { - if (isCrossRowTd) { - cordKey[rowIndex][colIndex].push(rowIndexToKey[i]) - } - for (let j = colIndex; j < colIndex + mergedColSpan; ++j) { - if (i === rowIndex && j === colIndex) continue - if (!(i in cordToPass)) { - cordToPass[i] = [j] - } else { - cordToPass[i].push(j) - } - } - } - } - const hoverKey = isCrossRowTd ? this.hoverKey : null - const { ellipsis } = column + const { length: rowCount } = mergedData + + const renderRow = ( + rowInfo: RowRenderInfo, + rowIndex: number, + isVirtual: boolean + ): VNode => { + if ('isExpandedRow' in rowInfo) { + const { + tmNode: { key, rawNode } + } = rowInfo return ( - - {hasChildren && colIndex === firstContentfulColIndex - ? [ - repeat( - isSummary ? 0 : rowInfo.level, -
- ), - isSummary || !rowInfo.children ? ( -
- ) : ( - { - handleUpdateExpanded(rowKey) - }} - /> - ) - ] - : null} - {column.type === 'selection' ? ( - !isSummary ? ( - - handleCheckboxUpdateChecked( - rowInfo, - checked, - e.shiftKey - ) - } - /> - ) : null - ) : column.type === 'expand' ? ( - !isSummary ? ( - !column.expandable || - column.expandable?.(rowData, rowIndex) ? ( - handleUpdateExpanded(rowKey)} - /> - ) : null - ) : null - ) : ( - - )} - + + {renderExpand!(rawNode, rowIndex)} + + ) - }) + } + const { rawNode: rowData, key: rowKey } = rowInfo + const isSummary = 'summary' in rowInfo + const expanded = mergedExpandedRowKeySet.has(rowInfo.key) const props = rowProps ? rowProps(rowData, rowIndex) : undefined const mergedRowClassName = typeof rowClassName === 'string' @@ -601,43 +484,218 @@ export default defineComponent({ ]} {...props} > - {colNodes} + {cols.map((col, colIndex) => { + if (!isVirtual && rowIndex in cordToPass) { + const cordOfRowToPass = cordToPass[rowIndex] + const indexInCordOfRowToPass = + cordOfRowToPass.indexOf(colIndex) + if (~indexInCordOfRowToPass) { + cordOfRowToPass.splice(indexInCordOfRowToPass, 1) + return null + } + } + + // TODO: Simplify row calculation + const { column } = col + const colKey = getColKey(col) + const { rowSpan, colSpan } = column + const mergedColSpan = isSummary + ? rowInfo.rawNode[colKey]?.colSpan || 1 // optional for #1276 + : colSpan + ? colSpan(rowData, rowIndex) + : 1 + const mergedRowSpan = isSummary + ? rowInfo.rawNode[colKey]?.rowSpan || 1 // optional for #1276 + : rowSpan + ? rowSpan(rowData, rowIndex) + : 1 + const isLastCol = colIndex + mergedColSpan === colCount + const isLastRow = rowIndex + mergedRowSpan === rowCount + const isCrossRowTd = mergedRowSpan > 1 + if (isCrossRowTd) { + cordKey[rowIndex] = { + [colIndex]: [] + } + } + if (mergedColSpan > 1 || isCrossRowTd) { + for ( + let i = rowIndex; + i < rowIndex + mergedRowSpan; + ++i + ) { + if (isCrossRowTd) { + cordKey[rowIndex][colIndex].push(rowIndexToKey[i]) + } + for ( + let j = colIndex; + j < colIndex + mergedColSpan; + ++j + ) { + if (i === rowIndex && j === colIndex) continue + if (!(i in cordToPass)) { + cordToPass[i] = [j] + } else { + cordToPass[i].push(j) + } + } + } + } + const hoverKey = isCrossRowTd ? this.hoverKey : null + const { ellipsis } = column + return ( + + {hasChildren && colIndex === firstContentfulColIndex + ? [ + repeat( + isSummary ? 0 : rowInfo.level, +
+ ), + isSummary || !rowInfo.children ? ( +
+ ) : ( + { + handleUpdateExpanded(rowKey) + }} + /> + ) + ] + : null} + {column.type === 'selection' ? ( + !isSummary ? ( + + handleCheckboxUpdateChecked( + rowInfo, + checked, + e.shiftKey + ) + } + /> + ) : null + ) : column.type === 'expand' ? ( + !isSummary ? ( + !column.expandable || + column.expandable?.(rowData, rowIndex) ? ( + handleUpdateExpanded(rowKey)} + /> + ) : null + ) : null + ) : ( + + )} + + ) + })} ) - if (showExpandContent) { - rows.push( - row, - - - {renderExpand(rowData, rowIndex)} - - - ) + + return row + } + + // Tile the data of the expanded row + const displayedData: RowRenderInfo[] = [] + mergedData.forEach((rowInfo) => { + if (renderExpand && mergedExpandedRowKeySet.has(rowInfo.key)) { + displayedData.push(rowInfo, { + isExpandedRow: true, + key: rowInfo.key, + tmNode: rowInfo as TmNode + }) } else { - rows.push(row) + displayedData.push(rowInfo) } }) - // Please note that the current virtual scroll mode impl - // not very performant, since it supports all the feature of table. - // If we can bailout some path it will be much faster. Since it - // need to generate all vnodes before using the virtual list. - if (virtualScroll) { + if (!virtualScroll) { + return ( + + + {cols.map((col) => ( + + ))} + + {showHeader ? : null} + + {displayedData.map((rowInfo, rowIndex) => { + return renderRow(rowInfo, rowIndex, false) + })} + +
+ ) + } else { return ( {{ - default: ({ item }: { item: VNode }) => { - return item - } + default: ({ + item, + index + }: { + item: RowRenderInfo + index: number + }) => renderRow(item, index, true) }} ) } - - return ( - - - {cols.map((col) => ( - - ))} - - {showHeader ? : null} - - {rows} - -
- ) } }}