Skip to content

perf(cdk/drag-drop): avoid excessive change detections with zone-patch-rxjs #23272

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 4, 2021
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 59 additions & 47 deletions src/cdk/drag-drop/directives/drag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,51 +274,24 @@ export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
}

ngAfterViewInit() {
// We need to wait for the zone to stabilize, in order for the reference
// element to be in the proper place in the DOM. This is mostly relevant
// for draggable elements inside portals since they get stamped out in
// their original DOM position and then they get transferred to the portal.
this._ngZone.onStable
.pipe(take(1), takeUntil(this._destroyed))
.subscribe(() => {
this._updateRootElement();

// Listen for any newly-added handles.
this._handles.changes.pipe(
startWith(this._handles),
// Sync the new handles with the DragRef.
tap((handles: QueryList<CdkDragHandle>) => {
const childHandleElements = handles
.filter(handle => handle._parentDrag === this)
.map(handle => handle.element);

// Usually handles are only allowed to be a descendant of the drag element, but if
// the consumer defined a different drag root, we should allow the drag element
// itself to be a handle too.
if (this._selfHandle && this.rootElementSelector) {
childHandleElements.push(this.element);
}

this._dragRef.withHandles(childHandleElements);
}),
// Listen if the state of any of the handles changes.
switchMap((handles: QueryList<CdkDragHandle>) => {
return merge(...handles.map(item => {
return item._stateChanges.pipe(startWith(item));
})) as Observable<CdkDragHandle>;
}),
takeUntil(this._destroyed)
).subscribe(handleInstance => {
// Enabled/disable the handle that changed in the DragRef.
const dragRef = this._dragRef;
const handle = handleInstance.element.nativeElement;
handleInstance.disabled ? dragRef.disableHandle(handle) : dragRef.enableHandle(handle);
// Normally this isn't in the zone, but it can cause major performance regressions for apps
// using `zone-patch-rxjs` because it'll trigger a change detection when it unsubscribes.
this._ngZone.runOutsideAngular(() => {
// We need to wait for the zone to stabilize, in order for the reference
// element to be in the proper place in the DOM. This is mostly relevant
// for draggable elements inside portals since they get stamped out in
// their original DOM position and then they get transferred to the portal.
this._ngZone.onStable
.pipe(take(1), takeUntil(this._destroyed))
.subscribe(() => {
this._updateRootElement();
this._setupHandlesListener();

if (this.freeDragPosition) {
this._dragRef.setFreeDragPosition(this.freeDragPosition);
}
});

if (this.freeDragPosition) {
this._dragRef.setFreeDragPosition(this.freeDragPosition);
}
});
});
}

ngOnChanges(changes: SimpleChanges) {
Expand Down Expand Up @@ -346,9 +319,13 @@ export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
if (index > -1) {
CdkDrag._dragInstances.splice(index, 1);
}
this._destroyed.next();
this._destroyed.complete();
this._dragRef.dispose();

// Unnecessary in most cases, but used to avoid extra change detections with `zone-paths-rxjs`.
this._ngZone.runOutsideAngular(() => {
this._destroyed.next();
this._destroyed.complete();
this._dragRef.dispose();
});
}

/** Syncs the root element with the `DragRef`. */
Expand Down Expand Up @@ -536,6 +513,41 @@ export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
}
}

/** Sets up the listener that syncs the handles with the drag ref. */
private _setupHandlesListener() {
// Listen for any newly-added handles.
this._handles.changes.pipe(
startWith(this._handles),
// Sync the new handles with the DragRef.
tap((handles: QueryList<CdkDragHandle>) => {
const childHandleElements = handles
.filter(handle => handle._parentDrag === this)
.map(handle => handle.element);

// Usually handles are only allowed to be a descendant of the drag element, but if
// the consumer defined a different drag root, we should allow the drag element
// itself to be a handle too.
if (this._selfHandle && this.rootElementSelector) {
childHandleElements.push(this.element);
}

this._dragRef.withHandles(childHandleElements);
}),
// Listen if the state of any of the handles changes.
switchMap((handles: QueryList<CdkDragHandle>) => {
return merge(...handles.map(item => {
return item._stateChanges.pipe(startWith(item));
})) as Observable<CdkDragHandle>;
}),
takeUntil(this._destroyed)
).subscribe(handleInstance => {
// Enabled/disable the handle that changed in the DragRef.
const dragRef = this._dragRef;
const handle = handleInstance.element.nativeElement;
handleInstance.disabled ? dragRef.disableHandle(handle) : dragRef.enableHandle(handle);
});
}

static ngAcceptInputType_disabled: BooleanInput;
}

Expand Down