Merge pull request #8103 from 2betop/chore-table913

chore: 优化 Table 渲染单元格性能
This commit is contained in:
hsm-lv 2023-09-14 14:00:26 +08:00 committed by GitHub
commit 7c320edd56
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 289 additions and 229 deletions

View File

@ -785,7 +785,7 @@ export const TableStore = iRendererStore
},
get columnWidthReady() {
return getFilteredColumns().every(column => column.width);
return getFilteredColumns().every(column => column.realWidth);
},
getStickyStyles(column: IColumn, columns: Array<IColumn>) {
@ -1036,11 +1036,11 @@ export const TableStore = iRendererStore
const tableWidth = table.parentElement!.offsetWidth;
const thead = table.querySelector(':scope>thead')!;
let tbody: HTMLElement | null = null;
const div = document.createElement('div');
div.className = 'amis-scope'; // jssdk 里面 css 会在这一层
div.style.cssText += `visibility: hidden!important;`;
const htmls: Array<string> = [];
const isFixed = self.tableLayout === 'fixed';
const someSettedWidth = self.columns.some(
column => column.pristine.width
);
const minWidths: {
[propName: string]: number;
@ -1054,17 +1054,27 @@ export const TableStore = iRendererStore
);
}
htmls.push(
`<table style="table-layout:auto!important;min-width:${tableWidth}px!important;width:${tableWidth}px!important;" class="${table.className.replace(
'is-layout-fixed',
''
)}">${thead.outerHTML}${
tbody ? `<tbody>${tbody.innerHTML}</tbody>` : ''
}</table>`
);
if (someSettedWidth || isFixed) {
htmls.push(
`<table style="table-layout:auto!important;min-width:${tableWidth}px!important;width:${tableWidth}px!important;" class="${table.className.replace(
'is-layout-fixed',
''
)}">${thead.outerHTML}${
tbody ? `<tbody>${tbody.innerHTML}</tbody>` : ''
}</table>`
);
}
if (!htmls.length) {
return;
}
const div = document.createElement('div');
div.className = 'amis-scope'; // jssdk 里面 css 会在这一层
div.style.cssText += `visibility: hidden!important;`;
div.innerHTML = htmls.join('');
let ths1: Array<HTMLTableCellElement> = [];
let ths2: Array<HTMLTableCellElement> = [];
if (isFixed) {
ths1 = [].slice.call(
@ -1074,9 +1084,13 @@ export const TableStore = iRendererStore
);
}
const ths2: Array<HTMLTableCellElement> = [].slice.call(
div.querySelectorAll(':scope>table:last-child>thead>tr>th[data-index]')
);
if (someSettedWidth || isFixed) {
ths2 = [].slice.call(
div.querySelectorAll(
':scope>table:last-child>thead>tr>th[data-index]'
)
);
}
ths1.forEach(th => {
th.style.cssText += 'width: 0';
@ -1106,14 +1120,16 @@ export const TableStore = iRendererStore
ths2.forEach((col: HTMLElement) => {
const index = parseInt(col.getAttribute('data-index')!, 10);
const column = self.columns[index];
column.setWidth(
Math.max(
typeof column.pristine.width === 'number'
? column.pristine.width
: col.clientWidth,
minWidths[index] || 0
)
);
if (column.pristine.width || isFixed) {
column.setWidth(
Math.max(
typeof column.pristine.width === 'number'
? column.pristine.width
: col.clientWidth,
minWidths[index] || 0
)
);
}
});
document.body.removeChild(div);
@ -1134,10 +1150,6 @@ export const TableStore = iRendererStore
});
}
function invalidTableColumnWidth() {
self.columns.forEach(column => column.setWidth(0));
}
function combineCell(arr: Array<SRow>, keys: Array<string>): Array<SRow> {
if (!keys.length || !arr.length) {
return arr;
@ -1740,7 +1752,6 @@ export const TableStore = iRendererStore
updateColumns,
initTableWidth,
syncTableWidth,
invalidTableColumnWidth,
initRows,
updateSelected,
toggleAll,

View File

@ -259,7 +259,6 @@ exports[`doAction:crud reload 1`] = `
class="cxd-Spinner-icon cxd-Spinner-icon--default"
/>
</div>
<span />
</div>
<div
class="resize-sensor"
@ -568,7 +567,6 @@ exports[`doAction:crud reload with data1 1`] = `
class="cxd-Spinner-icon cxd-Spinner-icon--default"
/>
</div>
<span />
</div>
<div
class="resize-sensor"
@ -877,7 +875,6 @@ exports[`doAction:crud reload with data2 1`] = `
class="cxd-Spinner-icon cxd-Spinner-icon--default"
/>
</div>
<span />
</div>
<div
class="resize-sensor"

View File

@ -195,7 +195,6 @@ exports[`Renderer:input table 1`] = `
</tbody>
</table>
</div>
<span />
</div>
<div
class="resize-sensor"
@ -599,7 +598,6 @@ exports[`Renderer:input table add 1`] = `
</tbody>
</table>
</div>
<span />
</div>
<div
class="resize-sensor"
@ -928,7 +926,6 @@ exports[`Renderer:input-table cell selects delete 1`] = `
</tbody>
</table>
</div>
<span />
</div>
<div
class="resize-sensor"
@ -1664,7 +1661,6 @@ exports[`Renderer:input-table init display 1`] = `
</tbody>
</table>
</div>
<span />
</div>
<div
class="resize-sensor"
@ -2103,7 +2099,6 @@ exports[`Renderer:input-table with combo column 1`] = `
</tbody>
</table>
</div>
<span />
</div>
<div
class="resize-sensor"

View File

@ -487,7 +487,6 @@ exports[`1. Renderer:crud basic interval headerToolbar footerToolbar 1`] = `
class="cxd-Spinner-icon cxd-Spinner-icon--default"
/>
</div>
<span />
</div>
<div
class="cxd-Table-toolbar cxd-Table-footToolbar"
@ -2587,7 +2586,6 @@ exports[`6. Renderer:crud source & alwaysShowPagination 1`] = `
</tbody>
</table>
</div>
<span />
</div>
<div
class="cxd-Table-toolbar cxd-Table-footToolbar"
@ -3464,7 +3462,6 @@ exports[`13. enderer: crud keepItemSelectionOnPageChange & maxKeepItemSelectionL
</tbody>
</table>
</div>
<span />
</div>
<div
class="resize-sensor"

View File

@ -210,7 +210,6 @@ exports[`Renderer:Pagination 1`] = `
</tbody>
</table>
</div>
<span />
</div>
<div
class="resize-sensor"

View File

@ -323,7 +323,6 @@ exports[`Renderer:table 1`] = `
</tbody>
</table>
</div>
<span />
</div>
<div
class="resize-sensor"
@ -714,7 +713,6 @@ exports[`Renderer:table align 1`] = `
</tbody>
</table>
</div>
<span />
</div>
<div
class="resize-sensor"
@ -1307,7 +1305,6 @@ exports[`Renderer:table children 1`] = `
</tbody>
</table>
</div>
<span />
</div>
<div
class="resize-sensor"
@ -1680,7 +1677,6 @@ exports[`Renderer:table classNameExpr 1`] = `
</tbody>
</table>
</div>
<span />
</div>
<div
class="resize-sensor"
@ -2059,7 +2055,6 @@ exports[`Renderer:table column head style className 1`] = `
</tbody>
</table>
</div>
<span />
</div>
<div
class="resize-sensor"
@ -2631,7 +2626,6 @@ exports[`Renderer:table combine Renderer:table combineNum only 1`] = `
</tbody>
</table>
</div>
<span />
</div>
<div
class="resize-sensor"
@ -3278,7 +3272,6 @@ exports[`Renderer:table combine Renderer:table combineNum with fromIndex 1`] = `
</tbody>
</table>
</div>
<span />
</div>
<div
class="resize-sensor"
@ -4059,7 +4052,6 @@ exports[`Renderer:table groupName-default 1`] = `
</tbody>
</table>
</div>
<span />
</div>
<div
class="resize-sensor"
@ -4844,7 +4836,6 @@ exports[`Renderer:table groupName-middleNoGroupName 1`] = `
</tbody>
</table>
</div>
<span />
</div>
<div
class="resize-sensor"
@ -5617,7 +5608,6 @@ exports[`Renderer:table groupName-startNoGroupName 1`] = `
</tbody>
</table>
</div>
<span />
</div>
<div
class="resize-sensor"
@ -6398,7 +6388,6 @@ exports[`Renderer:table groupName-withTpl 1`] = `
</tbody>
</table>
</div>
<span />
</div>
<div
class="resize-sensor"
@ -6782,7 +6771,6 @@ exports[`Renderer:table isHead fixed 1`] = `
</tbody>
</table>
</div>
<span />
</div>
<div
class="resize-sensor"
@ -8014,7 +8002,6 @@ exports[`Renderer:table list 1`] = `
</tbody>
</table>
</div>
<span />
</div>
<div
class="resize-sensor"

View File

@ -0,0 +1,221 @@
import {
IColumn,
IRow,
ITableStore,
PlainObject,
SchemaNode,
ThemeProps,
resolveVariable
} from 'amis-core';
import {BadgeObject, Checkbox, Icon} from 'amis-ui';
import React from 'react';
export interface CellProps extends ThemeProps {
region: string;
column: IColumn;
item: IRow;
props: PlainObject;
ignoreDrag?: boolean;
render: (
region: string,
node: SchemaNode,
props?: PlainObject
) => JSX.Element;
store: ITableStore;
multiple: boolean;
canAccessSuperData?: boolean;
itemBadge?: BadgeObject;
onCheck?: (item: IRow) => void;
onDragStart?: (e: React.DragEvent) => void;
popOverContainer?: any;
quickEditFormRef: any;
onImageEnlarge?: any;
}
export default function Cell({
region,
column,
item,
props,
ignoreDrag,
render,
store,
multiple,
itemBadge,
classnames: cx,
classPrefix: ns,
canAccessSuperData,
onCheck,
onDragStart,
popOverContainer,
quickEditFormRef,
onImageEnlarge
}: CellProps) {
if (column.name && item.rowSpans[column.name] === 0) {
return null;
}
const [style, stickyClassName]: any = React.useMemo(() => {
const style = {...column.pristine.style};
const [stickyStyle, stickyClassName] = store.getStickyStyles(
column,
store.filteredColumns
);
return [Object.assign(style, stickyStyle), stickyClassName];
}, []);
const onCheckboxChange = React.useCallback(() => {
onCheck?.(item);
}, []);
if (column.type === '__checkme') {
return (
<td
style={style}
key={props.key}
className={cx(column.pristine.className, stickyClassName)}
>
<Checkbox
classPrefix={ns}
type={multiple ? 'checkbox' : 'radio'}
checked={item.checked}
disabled={item.checkdisable || !item.checkable}
onChange={onCheckboxChange}
/>
</td>
);
} else if (column.type === '__dragme') {
return (
<td
style={style}
key={props.key}
className={cx(column.pristine.className, stickyClassName, {
'is-dragDisabled': !item.draggable
})}
>
{item.draggable ? <Icon icon="drag" className="icon" /> : null}
</td>
);
} else if (column.type === '__expandme') {
return (
<td
style={style}
key={props.key}
className={cx(column.pristine.className, stickyClassName)}
>
{item.expandable ? (
<a
className={cx('Table-expandBtn', item.expanded ? 'is-active' : '')}
// data-tooltip="展开/收起"
// data-position="top"
onClick={item.toggleExpanded}
>
<Icon icon="right-arrow-bold" className="icon" />
</a>
) : null}
</td>
);
}
let [prefix, affix, addtionalClassName] = React.useMemo(() => {
let prefix: React.ReactNode[] = [];
let affix: React.ReactNode[] = [];
let addtionalClassName = '';
if (column.isPrimary && store.isNested) {
addtionalClassName = 'Table-primayCell';
prefix.push(
<span
key="indent"
className={cx('Table-indent')}
style={item.indentStyle}
/>
);
prefix.push(
item.expandable ? (
<a
key="expandBtn2"
className={cx('Table-expandBtn2', item.expanded ? 'is-active' : '')}
// data-tooltip="展开/收起"
// data-position="top"
onClick={item.toggleExpanded}
>
<Icon icon="right-arrow-bold" className="icon" />
</a>
) : (
<span key="expandSpace" className={cx('Table-expandSpace')} />
)
);
}
if (
!ignoreDrag &&
column.isPrimary &&
store.isNested &&
store.draggable &&
item.draggable
) {
affix.push(
<a
key="dragBtn"
draggable
onDragStart={onDragStart}
className={cx('Table-dragBtn')}
>
<Icon icon="drag" className="icon" />
</a>
);
}
return [prefix, affix, addtionalClassName];
}, [item.expandable, item.expanded]);
const data = React.useMemo(() => item.locals, [JSON.stringify(item.locals)]);
const finalCanAccessSuperData =
column.pristine.canAccessSuperData ?? canAccessSuperData;
const subProps: any = {
...props,
// 操作列不下发loading否则会导致操作栏里面的所有按钮都出现loading
loading: column.type === 'operation' ? false : props.loading,
btnDisabled: store.dragging,
data: data,
value: column.name
? resolveVariable(
column.name,
finalCanAccessSuperData ? item.locals : item.data
)
: column.value,
popOverContainer: popOverContainer,
rowSpan: item.rowSpans[column.name as string],
quickEditFormRef: quickEditFormRef,
cellPrefix: prefix,
cellAffix: affix,
onImageEnlarge: onImageEnlarge,
canAccessSuperData: finalCanAccessSuperData,
row: item,
itemBadge,
showBadge:
!props.isHead &&
itemBadge &&
store.firstToggledColumnIndex === props.colIndex,
onQuery: undefined,
style,
className: cx(
column.pristine.className,
stickyClassName,
addtionalClassName
),
/** 给子节点的设置默认值避免取到env.affixHeader的默认值导致表头覆盖首行 */
affixOffsetTop: 0
};
delete subProps.label;
return render(
region,
{
...column.pristine,
column: column.pristine,
type: 'cell'
},
subProps
);
}

View File

@ -13,9 +13,10 @@ export function ColGroup({
React.useEffect(() => {
if (domRef.current) {
store.initTableWidth();
store.syncTableWidth();
}
});
}, []);
React.useEffect(() => {
const table = domRef.current!.parentElement!;
@ -37,7 +38,7 @@ export function ColGroup({
{columns.map(column => {
const style: any = {};
if (store.columnWidthReady) {
if (store.columnWidthReady && column.width) {
style.width = column.width;
} else if (column.pristine.width) {
style.width = column.pristine.width;

View File

@ -179,6 +179,7 @@ export class TableRow extends React.PureComponent<
onEvent,
expanded,
parentExpanded,
id,
newIndex,
isHover,
@ -188,7 +189,9 @@ export class TableRow extends React.PureComponent<
depth,
expandable,
appeard,
checkdisable,
trRef,
isNested,
...rest
} = this.props;

View File

@ -69,6 +69,7 @@ import omit from 'lodash/omit';
import ColGroup from './ColGroup';
import debounce from 'lodash/debounce';
import AutoFilterForm from './AutoFilterForm';
import Cell from './Cell';
/**
*
@ -547,7 +548,6 @@ export default class Table extends React.Component<TableProps, object> {
this.tableRef = this.tableRef.bind(this);
this.affixedTableRef = this.affixedTableRef.bind(this);
this.updateTableInfo = this.updateTableInfo.bind(this);
this.updateTableInfoRef = this.updateTableInfoRef.bind(this);
this.handleAction = this.handleAction.bind(this);
this.handleCheck = this.handleCheck.bind(this);
this.handleCheckAll = this.handleCheckAll.bind(this);
@ -1238,13 +1238,6 @@ export default class Table extends React.Component<TableProps, object> {
callback && setTimeout(callback, 20);
}
updateTableInfoRef(ref: any) {
if (!ref) {
return;
}
this.updateTableInfo();
}
// 当表格滚动是,需要让 affixHeader 部分的表格也滚动
handleOutterScroll() {
const table = this.table as HTMLElement;
@ -2018,168 +2011,31 @@ export default class Table extends React.Component<TableProps, object> {
classnames: cx,
checkOnItemClick,
popOverContainer,
canAccessSuperData,
itemBadge
} = this.props;
if (column.name && item.rowSpans[column.name] === 0) {
return null;
}
const style: any = {...column.pristine.style};
const [stickyStyle, stickyClassName] = store.getStickyStyles(
column,
store.filteredColumns
);
Object.assign(style, stickyStyle);
if (column.type === '__checkme') {
return (
<td
style={style}
key={props.key}
className={cx(column.pristine.className, stickyClassName)}
>
<Checkbox
classPrefix={ns}
type={multiple ? 'checkbox' : 'radio'}
checked={item.checked}
disabled={item.checkdisable || !item.checkable}
onChange={this.handleCheck.bind(this, item)}
/>
</td>
);
} else if (column.type === '__dragme') {
return (
<td
style={style}
key={props.key}
className={cx(column.pristine.className, stickyClassName, {
'is-dragDisabled': !item.draggable
})}
>
{item.draggable ? <Icon icon="drag" className="icon" /> : null}
</td>
);
} else if (column.type === '__expandme') {
return (
<td
style={style}
key={props.key}
className={cx(column.pristine.className, stickyClassName)}
>
{item.expandable ? (
<a
className={cx(
'Table-expandBtn',
item.expanded ? 'is-active' : ''
)}
// data-tooltip="展开/收起"
// data-position="top"
onClick={item.toggleExpanded}
>
<Icon icon="right-arrow-bold" className="icon" />
</a>
) : null}
</td>
);
}
let prefix: React.ReactNode[] = [];
let affix: React.ReactNode[] = [];
let addtionalClassName = '';
if (column.isPrimary && store.isNested) {
addtionalClassName = 'Table-primayCell';
prefix.push(
<span
key="indent"
className={cx('Table-indent')}
style={item.indentStyle}
/>
);
prefix.push(
item.expandable ? (
<a
key="expandBtn2"
className={cx('Table-expandBtn2', item.expanded ? 'is-active' : '')}
// data-tooltip="展开/收起"
// data-position="top"
onClick={item.toggleExpanded}
>
<Icon icon="right-arrow-bold" className="icon" />
</a>
) : (
<span key="expandSpace" className={cx('Table-expandSpace')} />
)
);
}
if (
!ignoreDrag &&
column.isPrimary &&
store.isNested &&
store.draggable &&
item.draggable
) {
affix.push(
<a
key="dragBtn"
draggable
onDragStart={this.handleDragStart}
className={cx('Table-dragBtn')}
>
<Icon icon="drag" className="icon" />
</a>
);
}
const canAccessSuperData =
column.pristine.canAccessSuperData ?? this.props.canAccessSuperData;
const subProps: any = {
...props,
// 操作列不下发loading否则会导致操作栏里面的所有按钮都出现loading
loading: column.type === 'operation' ? false : props.loading,
btnDisabled: store.dragging,
data: item.locals,
value: column.name
? resolveVariable(
column.name,
canAccessSuperData ? item.locals : item.data
)
: column.value,
popOverContainer: this.getPopOverContainer,
rowSpan: item.rowSpans[column.name as string],
quickEditFormRef: this.subFormRef,
cellPrefix: prefix,
cellAffix: affix,
onImageEnlarge: this.handleImageEnlarge,
canAccessSuperData,
row: item,
itemBadge,
showBadge:
!props.isHead &&
itemBadge &&
store.firstToggledColumnIndex === props.colIndex,
onQuery: undefined,
style,
className: cx(
column.pristine.className,
stickyClassName,
addtionalClassName
),
/** 给子节点的设置默认值避免取到env.affixHeader的默认值导致表头覆盖首行 */
affixOffsetTop: 0
};
delete subProps.label;
return render(
region,
{
...column.pristine,
column: column.pristine,
type: 'cell'
},
subProps
return (
<Cell
key={props.key}
region={region}
column={column}
item={item}
props={props}
ignoreDrag={ignoreDrag}
render={render}
store={store}
multiple={multiple}
canAccessSuperData={canAccessSuperData}
classnames={cx}
classPrefix={ns}
itemBadge={itemBadge}
onCheck={this.handleCheck}
onDragStart={this.handleDragStart}
popOverContainer={this.getPopOverContainer}
quickEditFormRef={this.subFormRef}
onImageEnlarge={this.handleImageEnlarge}
/>
);
}
@ -2857,13 +2713,6 @@ export default class Table extends React.Component<TableProps, object> {
onMouseLeave={this.handleMouseLeave}
>
{this.renderTableContent()}
{
// 利用这个将 table-layout: auto 转成 table-layout: fixed
store.columnWidthReady ? null : (
<span ref={this.updateTableInfoRef} />
)
}
</div>
{footer}