perf: improve the struct tree performance when modifying

This commit is contained in:
MrWindlike 2022-06-27 10:37:46 +08:00
parent 8a7028ce56
commit 2e589609b7
3 changed files with 64 additions and 71 deletions

View File

@ -3,16 +3,15 @@ import { Box, Text, VStack } from '@chakra-ui/react';
import { ComponentSchema } from '@sunmao-ui/core';
import { ComponentItemView } from './ComponentItemView';
import { DropComponentWrapper } from './DropComponentWrapper';
import { ChildrenMap } from './StructureTree';
import { genOperation } from '../../operations';
import { EditorServices } from '../../types';
import { observer } from 'mobx-react-lite';
import { isEqual } from 'lodash';
type Props = {
component: ComponentSchema;
parentId: string | undefined;
slot: string | undefined;
childrenMap: ChildrenMap;
onSelectComponent: (id: string) => void;
onSelected?: (id: string) => void;
services: EditorServices;
@ -20,31 +19,14 @@ type Props = {
depth: number;
};
type ComponentTreeProps = Props & {
slotMap: Map<string, ComponentSchema[]>;
isSelected: boolean;
};
const observeSelected = (Component: React.FC<ComponentTreeProps>) => {
const ObserveActive: React.FC<Props> = props => {
const { services, component, onSelected } = props;
const { editorStore } = services;
const { selectedComponentId } = editorStore;
useEffect(() => {
if (selectedComponentId === component.id) {
onSelected?.(selectedComponentId);
}
}, [selectedComponentId, component.id, onSelected]);
return <Component {...props} isSelected={selectedComponentId === component.id} />;
};
return observer(ObserveActive);
};
const ComponentTree = (props: ComponentTreeProps) => {
const {
component,
childrenMap,
slotMap,
parentId,
slot,
isSelected,
@ -71,10 +53,10 @@ const ComponentTree = (props: ComponentTreeProps) => {
if (slots.length === 0) {
return undefined;
}
const children = childrenMap.get(component.id);
return slots.map(_slot => {
let slotContent;
const slotChildren = children?.get(_slot);
const slotChildren = slotMap.get(_slot);
if (slotChildren && slotChildren.length > 0) {
slotContent = slotChildren.map(c => {
return (
@ -83,7 +65,6 @@ const ComponentTree = (props: ComponentTreeProps) => {
component={c}
parentId={component.id}
slot={_slot}
childrenMap={childrenMap}
onSelectComponent={onSelectComponent}
onSelected={onChildSelected}
services={services}
@ -130,7 +111,7 @@ const ComponentTree = (props: ComponentTreeProps) => {
});
}, [
slots,
childrenMap,
slotMap,
component.id,
onSelectComponent,
onChildSelected,
@ -193,6 +174,51 @@ const ComponentTree = (props: ComponentTreeProps) => {
);
};
export const ComponentTreeWrapper: React.FC<Props> = observeSelected(
React.memo(ComponentTree)
const MemoComponentTree: React.FC<ComponentTreeProps> = React.memo(
ComponentTree,
(oldProps, props) => {
const { slotMap: oldSlotMap, ...oldRest } = oldProps;
const { slotMap, ...rest } = props;
const oldKeys = [...oldSlotMap.keys()];
const keys = [...slotMap.keys()];
// check whether adding or removing the components
const isHasSameSlots = oldKeys.length === keys.length;
// check whether the properties of the child components have changed
// if the properties aren't changed, then it must have the same object reference
const isSameSlotMap =
isHasSameSlots &&
oldKeys.every(key => {
const oldChildren = oldSlotMap.get(key) || [];
const children = slotMap.get(key) || [];
return (
oldChildren.length === children.length &&
oldChildren.every((oldComponent, i) => oldComponent === children[i])
);
});
return isSameSlotMap && isEqual(oldRest, rest);
}
);
export const ComponentTreeWrapper: React.FC<Props> = observer(props => {
const { services, component, onSelected } = props;
const { editorStore } = services;
const { selectedComponentId, resolvedComponents } = editorStore;
const { childrenMap } = resolvedComponents;
const slotMap = childrenMap.get(component.id);
useEffect(() => {
if (selectedComponentId === component.id) {
onSelected?.(selectedComponentId);
}
}, [selectedComponentId, component.id, onSelected]);
return (
<MemoComponentTree
{...props}
isSelected={selectedComponentId === component.id}
slotMap={slotMap || new Map()}
/>
);
});

View File

@ -3,10 +3,9 @@ import { ComponentSchema } from '@sunmao-ui/core';
import { Box, Text, VStack } from '@chakra-ui/react';
import { ComponentTreeWrapper } from './ComponentTree';
import { DropComponentWrapper } from './DropComponentWrapper';
import { resolveApplicationComponents } from '../../utils/resolveApplicationComponents';
import ErrorBoundary from '../ErrorBoundary';
import { EditorServices } from '../../types';
import { CORE_VERSION, CoreTraitName, CoreComponentName, memo } from '@sunmao-ui/shared';
import { CORE_VERSION, CoreComponentName } from '@sunmao-ui/shared';
import {
AutoComplete,
AutoCompleteInput,
@ -15,7 +14,6 @@ import {
type Item,
} from '@choc-ui/chakra-autocomplete';
import { css } from '@emotion/css';
import { isEqual } from 'lodash-es';
import scrollIntoView from 'scroll-into-view';
export type ChildrenMap = Map<string, SlotsMap>;
@ -32,47 +30,11 @@ const AutoCompleteStyle = css`
margin-top: 0;
margin-bottom: 0.5rem;
`;
const SLOT_TYPE = `${CORE_VERSION}/${CoreTraitName.Slot}`;
const memoResolveApplicationComponents = memo(
resolveApplicationComponents,
function ([preRealComponents = []] = [], [realComponents]) {
// if add or remove the component, the struct tree should update
if (preRealComponents.length !== realComponents.length) {
return false;
}
for (const i in realComponents) {
const preComponent = preRealComponents[i];
const component = realComponents[i];
if (preComponent !== component) {
// there are two situations would cause the ids are different
// 1. the component id is changed
// 2. the order of components is changed. example: [a, b] -> [b, a]
if (preComponent.id !== component.id) {
return false;
}
// should check the slot container whether is changed too because move a component to a slot may not change the order
const preSlotTrait = preComponent.traits.find(trait => trait.type === SLOT_TYPE);
const slotTrait = component.traits.find(trait => trait.type === SLOT_TYPE);
if (
!isEqual(preSlotTrait?.properties.container, slotTrait?.properties.container)
) {
return false;
}
}
}
return true;
}
);
export const StructureTree: React.FC<Props> = props => {
const [search, setSearch] = useState('');
const { components, onSelectComponent, services } = props;
const { editorStore } = services;
const wrapperRef = useRef<HTMLDivElement>(null);
const onSelectOption = useCallback(
@ -109,8 +71,7 @@ export const StructureTree: React.FC<Props> = props => {
}, [components]);
const componentEles = useMemo(() => {
const { topLevelComponents, childrenMap } =
memoResolveApplicationComponents(realComponents);
const { topLevelComponents } = editorStore.resolvedComponents;
return topLevelComponents.map(c => (
<ComponentTreeWrapper
@ -118,7 +79,6 @@ export const StructureTree: React.FC<Props> = props => {
component={c}
parentId={undefined}
slot={undefined}
childrenMap={childrenMap}
onSelectComponent={onSelectComponent}
onSelected={onSelected}
services={services}
@ -126,7 +86,7 @@ export const StructureTree: React.FC<Props> = props => {
depth={0}
/>
));
}, [realComponents, onSelectComponent, onSelected, services]);
}, [onSelectComponent, onSelected, services, editorStore.resolvedComponents]);
return (
<VStack

View File

@ -15,6 +15,7 @@ import { ExplorerMenuTabs, ToolMenuTabs } from '../constants/enum';
import { CORE_VERSION, CoreComponentName } from '@sunmao-ui/shared';
import { isEqual } from 'lodash';
import { resolveApplicationComponents } from '../utils/resolveApplicationComponents';
type EditingTarget = {
kind: 'app' | 'module';
@ -112,6 +113,12 @@ export class EditorStore {
this.updateCurrentEditingTarget('app', this.app.version, this.app.metadata.name);
}
get resolvedComponents() {
return resolveApplicationComponents(
this.components.filter(c => c.type !== `${CORE_VERSION}/${CoreComponentName.Dummy}`)
);
}
get app() {
return this.appStorage.app;
}