From 6d9b1fff55bcb3d8a6c5ef2b9250cfcee6ce6039 Mon Sep 17 00:00:00 2001 From: Artur Androsovych Date: Fri, 17 Dec 2021 05:20:00 +0200 Subject: [PATCH] perf(module:switch): reduce change detection cycles (#7105) --- components/switch/switch.component.ts | 85 +++++++++++++++++---------- components/switch/switch.spec.ts | 44 +++++++++++++- 2 files changed, 96 insertions(+), 33 deletions(-) diff --git a/components/switch/switch.component.ts b/components/switch/switch.component.ts index 67f67dab9a..85e42c6453 100644 --- a/components/switch/switch.component.ts +++ b/components/switch/switch.component.ts @@ -14,6 +14,7 @@ import { ElementRef, forwardRef, Input, + NgZone, OnDestroy, OnInit, Optional, @@ -22,7 +23,7 @@ import { ViewEncapsulation } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; -import { Subject } from 'rxjs'; +import { fromEvent, Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; import { NzConfigKey, NzConfigService, WithConfig } from 'ng-zorro-antd/core/config'; @@ -57,7 +58,6 @@ const NZ_CONFIG_MODULE_NAME: NzConfigKey = 'switch'; [class.ant-switch-small]="nzSize === 'small'" [class.ant-switch-rtl]="dir === 'rtl'" [nzWaveExtraNode]="true" - (keydown)="onKeyDown($event)" > @@ -72,10 +72,7 @@ const NZ_CONFIG_MODULE_NAME: NzConfigKey = 'switch';
- `, - host: { - '(click)': 'onHostClick($event)' - } + ` }) export class NzSwitchComponent implements ControlValueAccessor, AfterViewInit, OnDestroy, OnInit { readonly _nzModuleName: NzConfigKey = NZ_CONFIG_MODULE_NAME; @@ -87,7 +84,7 @@ export class NzSwitchComponent implements ControlValueAccessor, AfterViewInit, O isChecked = false; onChange: OnChangeType = () => {}; onTouched: OnTouchedType = () => {}; - @ViewChild('switchElement', { static: true }) private switchElement?: ElementRef; + @ViewChild('switchElement', { static: true }) switchElement!: ElementRef; @Input() @InputBoolean() nzLoading = false; @Input() @InputBoolean() nzDisabled = false; @Input() @InputBoolean() nzControl = false; @@ -99,13 +96,6 @@ export class NzSwitchComponent implements ControlValueAccessor, AfterViewInit, O private destroy$ = new Subject(); - onHostClick(e: MouseEvent): void { - e.preventDefault(); - if (!this.nzDisabled && !this.nzLoading && !this.nzControl) { - this.updateValue(!this.isChecked); - } - } - updateValue(value: boolean): void { if (this.isChecked !== value) { this.isChecked = value; @@ -113,43 +103,74 @@ export class NzSwitchComponent implements ControlValueAccessor, AfterViewInit, O } } - onKeyDown(e: KeyboardEvent): void { - if (!this.nzControl && !this.nzDisabled && !this.nzLoading) { - if (e.keyCode === LEFT_ARROW) { - this.updateValue(false); - e.preventDefault(); - } else if (e.keyCode === RIGHT_ARROW) { - this.updateValue(true); - e.preventDefault(); - } else if (e.keyCode === SPACE || e.keyCode === ENTER) { - this.updateValue(!this.isChecked); - e.preventDefault(); - } - } - } - focus(): void { - this.focusMonitor.focusVia(this.switchElement?.nativeElement, 'keyboard'); + this.focusMonitor.focusVia(this.switchElement.nativeElement, 'keyboard'); } blur(): void { - this.switchElement?.nativeElement.blur(); + this.switchElement.nativeElement.blur(); } constructor( public nzConfigService: NzConfigService, + private host: ElementRef, + private ngZone: NgZone, private cdr: ChangeDetectorRef, private focusMonitor: FocusMonitor, @Optional() private directionality: Directionality ) {} ngOnInit(): void { - this.directionality.change?.pipe(takeUntil(this.destroy$)).subscribe((direction: Direction) => { + this.directionality.change.pipe(takeUntil(this.destroy$)).subscribe((direction: Direction) => { this.dir = direction; this.cdr.detectChanges(); }); this.dir = this.directionality.value; + + this.ngZone.runOutsideAngular(() => { + fromEvent(this.host.nativeElement, 'click') + .pipe(takeUntil(this.destroy$)) + .subscribe(event => { + event.preventDefault(); + + if (this.nzControl || this.nzDisabled || this.nzLoading) { + return; + } + + this.ngZone.run(() => { + this.updateValue(!this.isChecked); + this.cdr.markForCheck(); + }); + }); + + fromEvent(this.switchElement.nativeElement, 'keydown') + .pipe(takeUntil(this.destroy$)) + .subscribe(event => { + if (this.nzControl || this.nzDisabled || this.nzLoading) { + return; + } + + const { keyCode } = event; + if (keyCode !== LEFT_ARROW && keyCode !== RIGHT_ARROW && keyCode !== SPACE && keyCode !== ENTER) { + return; + } + + event.preventDefault(); + + this.ngZone.run(() => { + if (keyCode === LEFT_ARROW) { + this.updateValue(false); + } else if (keyCode === RIGHT_ARROW) { + this.updateValue(true); + } else if (keyCode === SPACE || keyCode === ENTER) { + this.updateValue(!this.isChecked); + } + + this.cdr.markForCheck(); + }); + }); + }); } ngAfterViewInit(): void { diff --git a/components/switch/switch.spec.ts b/components/switch/switch.spec.ts index da65855ac1..c8ab59d979 100644 --- a/components/switch/switch.spec.ts +++ b/components/switch/switch.spec.ts @@ -1,6 +1,6 @@ import { BidiModule, Dir } from '@angular/cdk/bidi'; import { ENTER, LEFT_ARROW, RIGHT_ARROW, SPACE } from '@angular/cdk/keycodes'; -import { Component, DebugElement, TemplateRef, ViewChild } from '@angular/core'; +import { ApplicationRef, Component, DebugElement, TemplateRef, ViewChild } from '@angular/core'; import { ComponentFixture, fakeAsync, flush, TestBed, waitForAsync } from '@angular/core/testing'; import { FormBuilder, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'; import { By } from '@angular/platform-browser'; @@ -172,6 +172,48 @@ describe('switch', () => { fixture.detectChanges(); expect(switchElement.nativeElement.firstElementChild === document.activeElement).toBe(false); }); + describe('change detection behavior', () => { + it('should not run change detection on `click` events if the switch is disabled', () => { + testComponent.disabled = true; + fixture.detectChanges(); + + const appRef = TestBed.inject(ApplicationRef); + const event = new MouseEvent('click'); + + spyOn(appRef, 'tick'); + spyOn(event, 'preventDefault').and.callThrough(); + + switchElement.nativeElement.dispatchEvent(event); + + expect(appRef.tick).not.toHaveBeenCalled(); + expect(event.preventDefault).toHaveBeenCalled(); + }); + it('should not run change detection on `keydown` events if the switch is disabled', () => { + testComponent.disabled = true; + fixture.detectChanges(); + + const switchButton = switchElement.nativeElement.querySelector('.ant-switch'); + const appRef = TestBed.inject(ApplicationRef); + const event = new KeyboardEvent('keydown', { + keyCode: SPACE + }); + + spyOn(appRef, 'tick'); + spyOn(event, 'preventDefault').and.callThrough(); + + switchButton.dispatchEvent(event); + + expect(appRef.tick).not.toHaveBeenCalled(); + expect(event.preventDefault).not.toHaveBeenCalled(); + + testComponent.disabled = false; + fixture.detectChanges(); + + switchButton.dispatchEvent(event); + + expect(event.preventDefault).toHaveBeenCalled(); + }); + }); }); describe('template switch', () => { let fixture: ComponentFixture;