Skip to content

Commit 9a8d794

Browse files
authored
perf(module:resizable): do not run change detection on mousedown and touchstart events (#7170)
1 parent f0f52a4 commit 9a8d794

5 files changed

Lines changed: 69 additions & 53 deletions

File tree

‎components/resizable/resizable.directive.ts‎

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@ import {
1515
Output,
1616
Renderer2
1717
} from '@angular/core';
18-
import { fromEvent, Subject } from 'rxjs';
18+
import { fromEvent } from 'rxjs';
1919
import { takeUntil } from 'rxjs/operators';
2020

21+
import { NzDestroyService } from 'ng-zorro-antd/core/services';
2122
import { BooleanInput } from 'ng-zorro-antd/core/types';
2223
import { ensureInBounds, InputBoolean } from 'ng-zorro-antd/core/util';
2324

@@ -35,7 +36,7 @@ export interface NzResizeEvent {
3536
@Directive({
3637
selector: '[nz-resizable]',
3738
exportAs: 'nzResizable',
38-
providers: [NzResizableService],
39+
providers: [NzResizableService, NzDestroyService],
3940
host: {
4041
class: 'nz-resizable',
4142
'[class.nz-resizable-resizing]': 'resizing',
@@ -68,38 +69,38 @@ export class NzResizableDirective implements AfterViewInit, OnDestroy {
6869
private ghostElement: HTMLDivElement | null = null;
6970
private el!: HTMLElement;
7071
private sizeCache: NzResizeEvent | null = null;
71-
private destroy$ = new Subject<void>();
7272

7373
constructor(
7474
private elementRef: ElementRef<HTMLElement>,
7575
private renderer: Renderer2,
7676
private nzResizableService: NzResizableService,
7777
private platform: Platform,
78-
private ngZone: NgZone
78+
private ngZone: NgZone,
79+
private destroy$: NzDestroyService
7980
) {
80-
this.nzResizableService.handleMouseDown$.pipe(takeUntil(this.destroy$)).subscribe(event => {
81+
this.nzResizableService.handleMouseDownOutsideAngular$.pipe(takeUntil(this.destroy$)).subscribe(event => {
8182
if (this.nzDisabled) {
8283
return;
8384
}
8485
this.resizing = true;
8586
this.nzResizableService.startResizing(event.mouseEvent);
8687
this.currentHandleEvent = event;
8788
this.setCursor();
88-
this.nzResizeStart.emit({
89-
mouseEvent: event.mouseEvent
90-
});
89+
if (this.nzResizeStart.observers.length) {
90+
this.ngZone.run(() => this.nzResizeStart.emit({ mouseEvent: event.mouseEvent }));
91+
}
9192
this.elRect = this.el.getBoundingClientRect();
9293
});
9394

94-
this.nzResizableService.documentMouseUp$.pipe(takeUntil(this.destroy$)).subscribe(event => {
95+
this.nzResizableService.documentMouseUpOutsideAngular$.pipe(takeUntil(this.destroy$)).subscribe(event => {
9596
if (this.resizing) {
9697
this.resizing = false;
97-
this.nzResizableService.documentMouseUp$.next();
98+
this.nzResizableService.documentMouseUpOutsideAngular$.next();
9899
this.endResize(event);
99100
}
100101
});
101102

102-
this.nzResizableService.documentMouseMove$.pipe(takeUntil(this.destroy$)).subscribe(event => {
103+
this.nzResizableService.documentMouseMoveOutsideAngular$.pipe(takeUntil(this.destroy$)).subscribe(event => {
103104
if (this.resizing) {
104105
this.resize(event);
105106
}
@@ -312,21 +313,19 @@ export class NzResizableDirective implements AfterViewInit, OnDestroy {
312313
fromEvent(this.el, 'mouseenter')
313314
.pipe(takeUntil(this.destroy$))
314315
.subscribe(() => {
315-
this.nzResizableService.mouseEntered$.next(true);
316+
this.nzResizableService.mouseEnteredOutsideAngular$.next(true);
316317
});
317318

318319
fromEvent(this.el, 'mouseleave')
319320
.pipe(takeUntil(this.destroy$))
320321
.subscribe(() => {
321-
this.nzResizableService.mouseEntered$.next(false);
322+
this.nzResizableService.mouseEnteredOutsideAngular$.next(false);
322323
});
323324
});
324325
}
325326

326327
ngOnDestroy(): void {
327328
this.ghostElement = null;
328329
this.sizeCache = null;
329-
this.destroy$.next();
330-
this.destroy$.complete();
331330
}
332331
}

‎components/resizable/resizable.service.ts‎

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,20 @@ export class NzResizableService implements OnDestroy {
1717
private document: Document;
1818
private listeners = new Map<string, (event: MouseEvent | TouchEvent) => void>();
1919

20-
handleMouseDown$ = new Subject<NzResizeHandleMouseDownEvent>();
21-
documentMouseUp$ = new Subject<MouseEvent | TouchEvent>();
22-
documentMouseMove$ = new Subject<MouseEvent | TouchEvent>();
23-
mouseEntered$ = new Subject<boolean>();
20+
/**
21+
* The `OutsideAngular` prefix means that the subject will emit events outside of the Angular zone,
22+
* so that becomes a bit more descriptive for those who'll maintain the code in the future:
23+
* ```ts
24+
* nzResizableService.handleMouseDownOutsideAngular$.subscribe(event => {
25+
* console.log(Zone.current); // <root>
26+
* console.log(NgZone.isInAngularZone()); // false
27+
* });
28+
* ```
29+
*/
30+
handleMouseDownOutsideAngular$ = new Subject<NzResizeHandleMouseDownEvent>();
31+
documentMouseUpOutsideAngular$ = new Subject<MouseEvent | TouchEvent>();
32+
documentMouseMoveOutsideAngular$ = new Subject<MouseEvent | TouchEvent>();
33+
mouseEnteredOutsideAngular$ = new Subject<boolean>();
2434

2535
constructor(private ngZone: NgZone, @Inject(DOCUMENT) document: NzSafeAny) {
2636
this.document = document;
@@ -32,10 +42,10 @@ export class NzResizableService implements OnDestroy {
3242
const moveEvent = _isTouchEvent ? 'touchmove' : 'mousemove';
3343
const upEvent = _isTouchEvent ? 'touchend' : 'mouseup';
3444
const moveEventHandler = (e: MouseEvent | TouchEvent): void => {
35-
this.documentMouseMove$.next(e);
45+
this.documentMouseMoveOutsideAngular$.next(e);
3646
};
3747
const upEventHandler = (e: MouseEvent | TouchEvent): void => {
38-
this.documentMouseUp$.next(e);
48+
this.documentMouseUpOutsideAngular$.next(e);
3949
this.clearListeners();
4050
};
4151

@@ -57,10 +67,10 @@ export class NzResizableService implements OnDestroy {
5767
}
5868

5969
ngOnDestroy(): void {
60-
this.handleMouseDown$.complete();
61-
this.documentMouseUp$.complete();
62-
this.documentMouseMove$.complete();
63-
this.mouseEntered$.complete();
70+
this.handleMouseDownOutsideAngular$.complete();
71+
this.documentMouseUpOutsideAngular$.complete();
72+
this.documentMouseMoveOutsideAngular$.complete();
73+
this.mouseEnteredOutsideAngular$.complete();
6474
this.clearListeners();
6575
}
6676
}

‎components/resizable/resizable.spec.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,14 @@ describe('resizable', () => {
7777
expect(appRef.tick).toHaveBeenCalledTimes(0);
7878
});
7979

80+
it('should not run change detection on `mousedown` event on the `nz-resize-handle`', () => {
81+
const appRef = TestBed.inject(ApplicationRef);
82+
spyOn(appRef, 'tick');
83+
const nzResizeHandle = resizableEle.querySelector('nz-resize-handle')!;
84+
dispatchMouseEvent(nzResizeHandle, 'mousedown');
85+
expect(appRef.tick).toHaveBeenCalledTimes(0);
86+
});
87+
8088
it('should maximum size work', fakeAsync(() => {
8189
const rect = resizableEle.getBoundingClientRect();
8290
const handel = resizableEle.querySelector('.nz-resizable-handle-bottomRight') as HTMLElement;

‎components/resizable/resize-handle.component.ts‎

Lines changed: 26 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,16 @@ import {
99
ElementRef,
1010
EventEmitter,
1111
Input,
12-
OnDestroy,
12+
NgZone,
1313
OnInit,
1414
Output,
1515
Renderer2
1616
} from '@angular/core';
17-
import { Subject } from 'rxjs';
17+
import { fromEvent, merge } from 'rxjs';
1818
import { takeUntil } from 'rxjs/operators';
1919

20+
import { NzDestroyService } from 'ng-zorro-antd/core/services';
21+
2022
import { NzResizableService } from './resizable.service';
2123

2224
export type NzResizeDirection =
@@ -47,41 +49,42 @@ export class NzResizeHandleMouseDownEvent {
4749
'[class.nz-resizable-handle-topRight]': `nzDirection === 'topRight'`,
4850
'[class.nz-resizable-handle-bottomRight]': `nzDirection === 'bottomRight'`,
4951
'[class.nz-resizable-handle-bottomLeft]': `nzDirection === 'bottomLeft'`,
50-
'[class.nz-resizable-handle-topLeft]': `nzDirection === 'topLeft'`,
51-
'(mousedown)': 'onMousedown($event)',
52-
'(touchstart)': 'onMousedown($event)'
53-
}
52+
'[class.nz-resizable-handle-topLeft]': `nzDirection === 'topLeft'`
53+
},
54+
providers: [NzDestroyService]
5455
})
55-
export class NzResizeHandleComponent implements OnInit, OnDestroy {
56+
export class NzResizeHandleComponent implements OnInit {
5657
@Input() nzDirection: NzResizeDirection = 'bottomRight';
5758
@Output() readonly nzMouseDown = new EventEmitter<NzResizeHandleMouseDownEvent>();
5859

59-
private destroy$ = new Subject<void>();
60-
6160
constructor(
61+
private ngZone: NgZone,
6262
private nzResizableService: NzResizableService,
6363
private renderer: Renderer2,
64-
private elementRef: ElementRef
64+
private host: ElementRef<HTMLElement>,
65+
private destroy$: NzDestroyService
6566
) {}
6667

6768
ngOnInit(): void {
68-
// Caretaker note: `mouseEntered$` subject will emit events within the `<root>` zone,
69-
// see `NzResizableDirective#ngAfterViewInit`. There're event listeners are added within the `<root>` zone.
70-
this.nzResizableService.mouseEntered$.pipe(takeUntil(this.destroy$)).subscribe(entered => {
69+
this.nzResizableService.mouseEnteredOutsideAngular$.pipe(takeUntil(this.destroy$)).subscribe(entered => {
7170
if (entered) {
72-
this.renderer.addClass(this.elementRef.nativeElement, 'nz-resizable-handle-box-hover');
71+
this.renderer.addClass(this.host.nativeElement, 'nz-resizable-handle-box-hover');
7372
} else {
74-
this.renderer.removeClass(this.elementRef.nativeElement, 'nz-resizable-handle-box-hover');
73+
this.renderer.removeClass(this.host.nativeElement, 'nz-resizable-handle-box-hover');
7574
}
7675
});
77-
}
7876

79-
onMousedown(event: MouseEvent | TouchEvent): void {
80-
this.nzResizableService.handleMouseDown$.next(new NzResizeHandleMouseDownEvent(this.nzDirection, event));
81-
}
82-
83-
ngOnDestroy(): void {
84-
this.destroy$.next();
85-
this.destroy$.complete();
77+
this.ngZone.runOutsideAngular(() => {
78+
merge(
79+
fromEvent<MouseEvent>(this.host.nativeElement, 'mousedown'),
80+
fromEvent<TouchEvent>(this.host.nativeElement, 'touchstart')
81+
)
82+
.pipe(takeUntil(this.destroy$))
83+
.subscribe((event: MouseEvent | TouchEvent) => {
84+
this.nzResizableService.handleMouseDownOutsideAngular$.next(
85+
new NzResizeHandleMouseDownEvent(this.nzDirection, event)
86+
);
87+
});
88+
});
8689
}
8790
}

‎components/resizable/resize-handles.component.ts‎

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,7 @@ export const DEFAULT_RESIZE_DIRECTION: NzResizeDirection[] = [
2626
})
2727
export class NzResizeHandlesComponent implements OnChanges {
2828
@Input() nzDirections: NzResizeDirection[] = DEFAULT_RESIZE_DIRECTION;
29-
directions: Set<NzResizeDirection>;
30-
31-
constructor() {
32-
this.directions = new Set(this.nzDirections);
33-
}
29+
directions = new Set<NzResizeDirection>(this.nzDirections);
3430

3531
ngOnChanges(changes: SimpleChanges): void {
3632
if (changes.nzDirections) {

0 commit comments

Comments
 (0)