Skip to content

Commit

Permalink
perf(module:resizable): reduce change detection cycles (#7036)
Browse files Browse the repository at this point in the history
  • Loading branch information
arturovt committed Nov 12, 2021
1 parent 7e587d1 commit 5cad154
Show file tree
Hide file tree
Showing 3 changed files with 54 additions and 36 deletions.
64 changes: 39 additions & 25 deletions components/resizable/resizable.directive.ts
Expand Up @@ -15,7 +15,7 @@ import {
Output,
Renderer2
} from '@angular/core';
import { Subject } from 'rxjs';
import { fromEvent, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';

import { BooleanInput } from 'ng-zorro-antd/core/types';
Expand All @@ -38,9 +38,7 @@ export interface NzResizeEvent {
providers: [NzResizableService],
host: {
'[class.nz-resizable-resizing]': 'resizing',
'[class.nz-resizable-disabled]': 'nzDisabled',
'(mouseenter)': 'onMouseenter()',
'(mouseleave)': 'onMouseleave()'
'[class.nz-resizable-disabled]': 'nzDisabled'
}
})
export class NzResizableDirective implements AfterViewInit, OnDestroy {
Expand Down Expand Up @@ -109,14 +107,6 @@ export class NzResizableDirective implements AfterViewInit, OnDestroy {
});
}

onMouseenter(): void {
this.nzResizableService.mouseEntered$.next(true);
}

onMouseleave(): void {
this.nzResizableService.mouseEntered$.next(false);
}

setPosition(): void {
const position = getComputedStyle(this.el).position;
if (position === 'static' || !position) {
Expand Down Expand Up @@ -252,12 +242,16 @@ export class NzResizableDirective implements AfterViewInit, OnDestroy {
}
const size = this.calcSize(width, height, ratio);
this.sizeCache = { ...size };
this.ngZone.run(() => {
this.nzResize.emit({
...size,
mouseEvent: event
// Re-enter the Angular zone and run the change detection only if there're any `nzResize` listeners,
// e.g.: `<div nz-resizable (nzResize)="..."></div>`.
if (this.nzResize.observers.length) {
this.ngZone.run(() => {
this.nzResize.emit({
...size,
mouseEvent: event
});
});
});
}
if (this.nzPreview) {
this.previewResize(size);
}
Expand All @@ -273,12 +267,16 @@ export class NzResizableDirective implements AfterViewInit, OnDestroy {
width: this.elRect.width,
height: this.elRect.height
};
this.ngZone.run(() => {
this.nzResizeEnd.emit({
...size,
mouseEvent: event
// Re-enter the Angular zone and run the change detection only if there're any `nzResizeEnd` listeners,
// e.g.: `<div nz-resizable (nzResizeEnd)="..."></div>`.
if (this.nzResizeEnd.observers.length) {
this.ngZone.run(() => {
this.nzResizeEnd.emit({
...size,
mouseEvent: event
});
});
});
}
this.sizeCache = null;
this.currentHandleEvent = null;
}
Expand All @@ -304,10 +302,26 @@ export class NzResizableDirective implements AfterViewInit, OnDestroy {
}

ngAfterViewInit(): void {
if (this.platform.isBrowser) {
this.el = this.elementRef.nativeElement;
this.setPosition();
if (!this.platform.isBrowser) {
return;
}

this.el = this.elementRef.nativeElement;
this.setPosition();

this.ngZone.runOutsideAngular(() => {
fromEvent(this.el, 'mouseenter')
.pipe(takeUntil(this.destroy$))
.subscribe(() => {
this.nzResizableService.mouseEntered$.next(true);
});

fromEvent(this.el, 'mouseleave')
.pipe(takeUntil(this.destroy$))
.subscribe(() => {
this.nzResizableService.mouseEntered$.next(false);
});
});
}

ngOnDestroy(): void {
Expand Down
9 changes: 5 additions & 4 deletions components/resizable/resizable.spec.ts
@@ -1,4 +1,4 @@
import { Component, ElementRef, NgZone, ViewChild } from '@angular/core';
import { ApplicationRef, Component, ElementRef, NgZone, ViewChild } from '@angular/core';
import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { By } from '@angular/platform-browser';

Expand Down Expand Up @@ -61,19 +61,20 @@ describe('resizable', () => {
});
});

it('should add hover class when mouseenter', () => {
it('should toggle the `nz-resizable-handle-box-hover` class when `mouseenter` and `mouseleave` events are fired and should not run change detection', () => {
const appRef = TestBed.inject(ApplicationRef);
spyOn(appRef, 'tick');
dispatchMouseEvent(resizableEle, 'mouseenter');
fixture.detectChanges();
const handles = resizableEle.querySelectorAll('.nz-resizable-handle');
expect(handles.length).toBe(8);
handles.forEach(e => {
expect(e.classList).toContain('nz-resizable-handle-box-hover');
});
dispatchMouseEvent(resizableEle, 'mouseleave');
fixture.detectChanges();
handles.forEach(e => {
expect(e.classList).not.toContain('nz-resizable-handle-box-hover');
});
expect(appRef.tick).toHaveBeenCalledTimes(0);
});

it('should maximum size work', fakeAsync(() => {
Expand Down
17 changes: 10 additions & 7 deletions components/resizable/resize-handle.component.ts
Expand Up @@ -5,14 +5,14 @@

import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ElementRef,
EventEmitter,
Input,
OnDestroy,
OnInit,
Output
Output,
Renderer2
} from '@angular/core';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
Expand Down Expand Up @@ -47,7 +47,6 @@ export class NzResizeHandleMouseDownEvent {
'[class.nz-resizable-handle-bottomRight]': `nzDirection === 'bottomRight'`,
'[class.nz-resizable-handle-bottomLeft]': `nzDirection === 'bottomLeft'`,
'[class.nz-resizable-handle-topLeft]': `nzDirection === 'topLeft'`,
'[class.nz-resizable-handle-box-hover]': 'entered',
'(mousedown)': 'onMousedown($event)',
'(touchstart)': 'onMousedown($event)'
}
Expand All @@ -56,22 +55,26 @@ export class NzResizeHandleComponent implements OnInit, OnDestroy {
@Input() nzDirection: NzResizeDirection = 'bottomRight';
@Output() readonly nzMouseDown = new EventEmitter<NzResizeHandleMouseDownEvent>();

entered = false;
private destroy$ = new Subject<void>();

constructor(
private nzResizableService: NzResizableService,
private cdr: ChangeDetectorRef,
private renderer: Renderer2,
private elementRef: ElementRef
) {
// TODO: move to host after View Engine deprecation
this.elementRef.nativeElement.classList.add('nz-resizable-handle');
}

ngOnInit(): void {
// Caretaker note: `mouseEntered$` subject will emit events within the `<root>` zone,
// see `NzResizableDirective#ngAfterViewInit`. There're event listeners are added within the `<root>` zone.
this.nzResizableService.mouseEntered$.pipe(takeUntil(this.destroy$)).subscribe(entered => {
this.entered = entered;
this.cdr.markForCheck();
if (entered) {
this.renderer.addClass(this.elementRef.nativeElement, 'nz-resizable-handle-box-hover');
} else {
this.renderer.removeClass(this.elementRef.nativeElement, 'nz-resizable-handle-box-hover');
}
});
}

Expand Down

0 comments on commit 5cad154

Please sign in to comment.