Browse Source

Improve loading performance (#6755)

* Cache list counter

* Skip model resolver when possible

* Clean up Component.getView

* Cleanup SelectComponent

* Update Component getView

* Check in CommandAbstract
release-v0.22.16
Artur Arseniev 5 months ago
committed by GitHub
parent
commit
48be01b860
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      packages/core/src/commands/view/CommandAbstract.ts
  2. 7
      packages/core/src/commands/view/SelectComponent.ts
  3. 50
      packages/core/src/dom_components/model/Component.ts
  4. 3
      packages/core/src/dom_components/model/Components.ts
  5. 48
      packages/core/src/dom_components/model/ModelDataResolverWatchers.ts
  6. 4
      packages/core/src/dom_components/model/ModelResolverWatcher.ts
  7. 11
      packages/core/src/domain_abstract/model/StyleableModel.ts
  8. 9
      packages/core/src/undo_manager/index.ts
  9. 21
      packages/core/test/specs/commands/index.ts
  10. 16
      packages/core/test/specs/undo_manager/index.ts

2
packages/core/src/commands/view/CommandAbstract.ts

@ -160,7 +160,7 @@ export default class CommandAbstract<O extends ObjectAny = any> extends Model {
* Stop current command
*/
stopCommand(opts?: any) {
this.em.Commands.stop(this.id as string, opts);
this.em?.Commands?.stop?.(this.id as string, opts);
}
/**

7
packages/core/src/commands/view/SelectComponent.ts

@ -69,6 +69,10 @@ export default {
* */
stopSelectComponent() {
this.toggleSelectComponent();
this.onContainerChange.cancel?.();
this.onSelect.cancel?.();
this.updateAttached.cancel?.();
this._upToolbar.cancel?.();
},
/**
@ -708,7 +712,6 @@ export default {
this.stopSelectComponent();
!opts.preserveSelected && em.setSelected();
this.toggleToolsEl();
this.updateAttached.cancel();
editor && editor.stopCommand('resize');
editor?.stopCommand('resize');
},
} as CommandObject<any, { [k: string]: any }>;

50
packages/core/src/dom_components/model/Component.ts

@ -95,6 +95,8 @@ type GetComponentStyleOpts = GetStyleOpts & {
inline?: boolean;
};
const idsMapCounter = Symbol('idsMapCounter');
/**
* The Component object represents a single node of our template structure, so when you update its properties the changes are
* immediately reflected on the canvas and in the code to export (indeed, when you ask to export the code we just go through all
@ -412,11 +414,17 @@ export default class Component extends StyleableModel<ComponentProperties> {
const { em } = this;
const um = em?.UndoManager;
const comps = this.components();
if (um && !this.__hasUm) {
if (!um || this.__hasUm) {
return;
}
if (um) {
um.add(comps);
um.add(this.getSelectors());
this.__hasUm = true;
}
opts.recursive && comps.map((c) => c.__postAdd(opts));
}
@ -1754,14 +1762,7 @@ export default class Component extends StyleableModel<ComponentProperties> {
* @return {ComponentView}
*/
getView(frame?: Frame) {
let { view, views, em } = this;
const frm = frame || em?.getCurrentFrameModel();
if (frm) {
view = views.filter((view) => view.frameView === frm.view)[0];
}
return view;
return super.getView(frame) as ComponentView | undefined;
}
getCurrentView() {
@ -2072,7 +2073,7 @@ export default class Component extends StyleableModel<ComponentProperties> {
const current = list[id];
if (!current) {
list[id] = model;
Component.setListId(list, id, model);
} else if (current !== model) {
const keepIdsCrossPages = model.em?.Components.config.keepAttributeIdsCrossPages;
const currentPage = current.page;
@ -2132,12 +2133,12 @@ export default class Component extends StyleableModel<ComponentProperties> {
idMap[currentId] = nextId;
}
list[nextId] = model;
Component.setListId(list, nextId, model);
return nextId;
}
static getNewId(list: ObjectAny) {
const count = Object.keys(list).length;
const count = Component.getListCount(list);
const ilen = count.toString().length + 2;
const uid = (Math.random() + 1.1).toString(36).slice(-ilen);
let newId = `i${uid}`;
@ -2149,6 +2150,31 @@ export default class Component extends StyleableModel<ComponentProperties> {
return newId;
}
static getListCount(list: ObjectAny) {
const listWithCounter = list as ObjectAny & { [idsMapCounter]?: number };
let count = listWithCounter[idsMapCounter];
if (isUndefined(count)) {
count = Object.keys(list).length;
Object.defineProperty(list, idsMapCounter, {
configurable: true,
writable: true,
value: count,
});
}
return count;
}
static setListId(list: ObjectAny, id: string, model: Component) {
if (!list[id]) {
const listWithCounter = list as ObjectAny & { [idsMapCounter]?: number };
listWithCounter[idsMapCounter] = Component.getListCount(list) + 1;
}
list[id] = model;
}
static getIncrementId(id: string, list: ObjectAny, opts: { keepIds?: string[] } = {}) {
const { keepIds = [] } = opts;
let counter = 1;

3
packages/core/src/dom_components/model/Components.ts

@ -405,7 +405,8 @@ Component> {
onAdd(model: Component, c?: any, opts: { temporary?: boolean } = {}) {
const { domc, em } = this;
const avoidInline = em.config.avoidInlineStyle;
domc && domc.Component.ensureInList(model);
const allById = domc?.allById();
allById?.[model.getId()] !== model && domc?.Component.ensureInList(model);
if (!avoidInline && em.config.forceClass && !opts.temporary) {
const style = model.getStyle();

48
packages/core/src/dom_components/model/ModelDataResolverWatchers.ts

@ -9,6 +9,7 @@ import { getSymbolsToUpdate, isSymbol } from './SymbolUtils';
import Component, { keySymbolOvrd } from './Component';
import { StyleableModelProperties } from '../../domain_abstract/model/StyleableModel';
import { isEmpty, isObject } from 'underscore';
import { isDataResolverProps } from '../../data_sources/utils';
export const updateFromWatcher = { fromDataSource: true, avoidStore: true };
export const keyDataValues = '__data_values';
@ -63,6 +64,25 @@ export class ModelDataResolverWatchers<T extends StyleableModelProperties> {
return evaluatedProps;
}
shouldResolveProps(props: ObjectAny) {
if (this.hasDataResolvers()) return true;
if (this.hasDataValues(props[keyDataValues])) return true;
if (this.hasResolverValue(props.attributes) || this.hasResolverValue(props.style)) return true;
const { excludedFromEvaluation } = this;
const propKeys = Object.keys(props);
for (let index = 0; index < propKeys.length; index++) {
const key = propKeys[index];
if (!excludedFromEvaluation.includes(key) && isDataResolverProps(props[key])) {
return true;
}
}
return false;
}
getProps(data: ObjectAny): ObjectAny {
const resolvedProps = this.getValueOrResolver('props', data);
const result = {
@ -137,6 +157,24 @@ export class ModelDataResolverWatchers<T extends StyleableModelProperties> {
return [this.propertyWatcher, this.styleWatcher, this.attributeWatcher];
}
private get excludedFromEvaluation() {
return ['components', 'dataResolver', 'status', 'state', 'open', keySymbolOvrd, keyDataValues];
}
private hasDataResolvers() {
return this.watchers.some((watcher) => watcher.hasDataResolvers());
}
private hasDataValues(dataValues: ObjectAny | undefined) {
return Object.values(dataValues || {}).some((value) => isObject(value) && !isEmpty(value));
}
private hasResolverValue(values: ObjectAny | string | undefined) {
if (!isObject(values)) return false;
return Object.values(values).some(isDataResolverProps);
}
private isComponent(model: any): model is Component {
return model instanceof Component;
}
@ -187,15 +225,7 @@ export class ModelDataResolverWatchers<T extends StyleableModelProperties> {
}
private filterProps(props: ObjectAny) {
const excludedFromEvaluation = [
'components',
'dataResolver',
'status',
'state',
'open',
keySymbolOvrd,
keyDataValues,
];
const { excludedFromEvaluation } = this;
const filteredProps = Object.fromEntries(
Object.entries(props).filter(([key]) => !excludedFromEvaluation.includes(key)),
);

4
packages/core/src/dom_components/model/ModelResolverWatcher.ts

@ -245,6 +245,10 @@ export class ModelResolverWatcher<T extends ObjectHash> {
return serializableValues;
}
hasDataResolvers() {
return Object.keys(this.resolverListeners).length > 0;
}
getValuesResolvingFromCollections() {
const keys = Object.keys(this.resolverListeners).filter((key: string) => {
return this.resolverListeners[key].resolver.resolvesFromCollection();

11
packages/core/src/domain_abstract/model/StyleableModel.ts

@ -47,6 +47,7 @@ type WithDataResolvers<T> = {
export default class StyleableModel<T extends StyleableModelProperties = any> extends Model<T, UpdateStyleOptions> {
em?: EditorModel;
views: StyleableView[] = [];
view?: StyleableView;
dataResolverWatchers: ModelDataResolverWatchers<T>;
collectionsStateMap: DataCollectionStateMap = {};
opt: { em?: EditorModel };
@ -98,7 +99,10 @@ export default class StyleableModel<T extends StyleableModelProperties = any> ex
}
this.dataResolverWatchers = this.dataResolverWatchers ?? options.dataResolverWatchers;
const evaluatedValues = this.dataResolverWatchers.addProps(attributes, options) as Partial<T>;
const shouldResolveProps = this.dataResolverWatchers.shouldResolveProps(attributes);
const evaluatedValues: Partial<T> = shouldResolveProps
? (this.dataResolverWatchers.addProps(attributes, options) as Partial<T>)
: (attributes as Partial<T>);
return super.set(evaluatedValues, options);
}
@ -213,9 +217,10 @@ export default class StyleableModel<T extends StyleableModelProperties = any> ex
}
getView(frame?: Frame) {
let { views, em } = this;
let { em, view } = this;
const views = this.views || [];
const frm = frame || em?.getCurrentFrameModel();
return frm ? views.find((v) => v.frameView === frm.view) : views[0];
return frm ? views.find((v) => v.frameView === frm.view) : views[0] || view;
}
setView(view: StyleableView) {

9
packages/core/src/undo_manager/index.ts

@ -148,9 +148,13 @@ export default class UndoManagerModule extends Module<UndoManagerConfig & { name
[EditorEvents.undo, EditorEvents.redo].forEach((ev) => this.um.on(ev, () => em.trigger(ev)));
}
get isDisabled() {
return !!this.config._disable;
}
postLoad() {
const { config, em } = this;
config.trackSelection && em && this.add(em.get('selected'));
config.trackSelection && !this.isDisabled && em && this.add(em.get('selected'));
}
/**
@ -169,6 +173,7 @@ export default class UndoManagerModule extends Module<UndoManagerConfig & { name
* um.add(someModelOrCollection);
*/
add(entity: any) {
if (this.isDisabled) return this;
this.um.register(entity);
return this;
}
@ -181,6 +186,7 @@ export default class UndoManagerModule extends Module<UndoManagerConfig & { name
* um.remove(someModelOrCollection);
*/
remove(entity: any) {
if (this.isDisabled) return this;
this.um.unregister(entity);
return this;
}
@ -192,6 +198,7 @@ export default class UndoManagerModule extends Module<UndoManagerConfig & { name
* um.removeAll();
*/
removeAll() {
if (this.isDisabled) return this;
this.um.unregisterAll();
return this;
}

21
packages/core/test/specs/commands/index.ts

@ -48,6 +48,27 @@ describe('Commands', () => {
expect(obj.get('select-comp')).not.toBeUndefined();
});
test('Select component command cancels pending debounced callbacks on teardown', () => {
const command = obj.get('select-comp') as any;
const cancelOnContainerChange = jest.fn();
const cancelOnSelect = jest.fn();
const cancelUpdateAttached = jest.fn();
const cancelToolbar = jest.fn();
command.toggleSelectComponent = jest.fn();
command.onContainerChange = { cancel: cancelOnContainerChange };
command.onSelect = { cancel: cancelOnSelect };
command.updateAttached = { cancel: cancelUpdateAttached };
command._upToolbar = { cancel: cancelToolbar };
command.stopSelectComponent();
expect(command.toggleSelectComponent).toHaveBeenCalledWith();
expect(cancelOnContainerChange).toHaveBeenCalledTimes(1);
expect(cancelOnSelect).toHaveBeenCalledTimes(1);
expect(cancelUpdateAttached).toHaveBeenCalledTimes(1);
expect(cancelToolbar).toHaveBeenCalledTimes(1);
});
test('Commands module should not have toLoad property', () => {
// @ts-ignore
expect(obj.toLoad).toBeUndefined();

16
packages/core/test/specs/undo_manager/index.ts

@ -25,6 +25,22 @@ describe('Undo Manager', () => {
expect(um.getStack()).toHaveLength(0);
});
test('Disabled undo manager does not track changes', () => {
const { editor, um } = setupTestEditor({
withCanvas: true,
config: { undoManager: false },
});
const wrapper = editor.getWrapper()!;
wrapper.append('<div></div>');
expect(um.isDisabled).toBe(true);
expect(um.hasUndo()).toBe(false);
expect(um.getStack()).toHaveLength(0);
editor.destroy();
});
describe('Component changes', () => {
test('Add component', () => {
expect(wrapper.components()).toHaveLength(0);

Loading…
Cancel
Save