javascript - Angular 2+ och debounce
I AngularJS kunde jag använda ng-model-alternativ för att debounce en modell.
ng-model-options="{ debounce: 1000 }"
Hur kan jag batta en modell i Angular?
Jag försökte söka efter debounce i dokumentationen men jag kunde inte hitta något.
https://angular.io/search/#stq=debounce&stp=1
En lösning skulle vara att skriva min egen debounce-funktion, till exempel:
import {Component, Template, bootstrap} from 'angular2/angular2';
// Annotation section
@Component({
selector: 'my-app'
})
@Template({
url: 'app.html'
})
// Component controller
class MyAppComponent {
constructor() {
this.firstName = 'Name';
}
changed($event, el){
console.log("changes", this.name, el.value);
this.name = el.value;
}
firstNameChanged($event, first){
if (this.timeoutId) window.clearTimeout(this.timeoutID);
this.timeoutID = window.setTimeout(() => {
this.firstName = first.value;
}, 250)
}
}
bootstrap(MyAppComponent);
Och min html
<input type=text [value]="firstName" #first (keyup)="firstNameChanged($event, first)">
Men jag letar efter en inbyggd funktion, finns det en i Angular?
Svar #1
Uppdaterad för RC.5
Med Angular 2 kan vi använda debouncing med RxJS-operatören debounceTime() på ett formulärkontrolls valueChanges-observable:
import {Component} from '@angular/core';
import {FormControl} from '@angular/forms';
import {Observable} from 'rxjs/Observable';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/throttleTime';
import 'rxjs/add/observable/fromEvent';
@Component({
selector: 'my-app',
template: `<input type=text [value]="firstName" [formControl]="firstNameControl">
<br>{{firstName}}`
})
export class AppComponent {
firstName = 'Name';
firstNameControl = new FormControl();
formCtrlSub: Subscription;
resizeSub: Subscription;
ngOnInit() {
// debounce keystroke events
this.formCtrlSub = this.firstNameControl.valueChanges
.debounceTime(1000)
.subscribe(newValue => this.firstName = newValue);
// throttle resize events
this.resizeSub = Observable.fromEvent(window, 'resize')
.throttleTime(200)
.subscribe(e => {
console.log('resize event', e);
this.firstName += '*'; // change something to show it worked
});
}
ngDoCheck() { console.log('change detection'); }
ngOnDestroy() {
this.formCtrlSub.unsubscribe();
this.resizeSub .unsubscribe();
}
}
Plunker
Koden ovan inkluderar också ett exempel på hur man reglerar fönsterstorleksändringsevent, som efterfrågats av @albanx i en kommentar nedan.
Även om koden ovan förmodligen är det Angular-sättet att göra det på, är det inte effektivt. Varje tangenttryckning och varje ändringshändelse av storlek, även om de är debounced och throttled, resulterar i att change detection körs. Med andra ord påverkar debouncing och throttling inte hur ofta change detection körs. (Jag hittade en kommentar på GitHub av Tobias Bosch som bekräftar detta.) Du kan se detta när du kör plunkern och ser hur många gånger ngDoCheck() anropas när du skriver i inmatningsrutan eller ändrar storlek på fönstret. (Använd den blå "x"-knappen för att köra plunkern i ett separat fönster för att se ändringshändelserna av storlek.)
En mer effektiv teknik är att skapa RxJS-observables själv från händelserna, utanför Angulars "zon". På detta sätt anropas inte ändringsdetektering varje gång en händelse inträffar. Sedan, i dina prenumerations-återropmetoder, utlös ändringsdetektering manuellt - dvs. du kontrollerar när ändringsdetektering anropas:
import {Component, NgZone, ChangeDetectorRef, ApplicationRef,
ViewChild, ElementRef} from '@angular/core';
import {Observable} from 'rxjs/Observable';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/throttleTime';
import 'rxjs/add/observable/fromEvent';
@Component({
selector: 'my-app',
template: `<input #input type=text [value]="firstName">
<br>{{firstName}}`
})
export class AppComponent {
firstName = 'Name';
keyupSub: Subscription;
resizeSub: Subscription;
@ViewChild('input') inputElRef: ElementRef;
constructor(private ngzone: NgZone, private cdref: ChangeDetectorRef,
private appref: ApplicationRef) {}
ngAfterViewInit() {
this.ngzone.runOutsideAngular( () => {
this.keyupSub = Observable.fromEvent(this.inputElRef.nativeElement, 'keyup')
.debounceTime(1000)
.subscribe(keyboardEvent => {
this.firstName = keyboardEvent.target.value;
this.cdref.detectChanges();
});
this.resizeSub = Observable.fromEvent(window, 'resize')
.throttleTime(200)
.subscribe(e => {
console.log('resize event', e);
this.firstName += '*'; // change something to show it worked
this.cdref.detectChanges();
});
});
}
ngDoCheck() { console.log('cd'); }
ngOnDestroy() {
this.keyupSub .unsubscribe();
this.resizeSub.unsubscribe();
}
}
Plunker
Jag använder ngAfterViewInit() istället för ngOnInit() för att säkerställa att inputElRef är definierad.
detectChanges() kommer att köra förändringsdetektering på denna komponent och dess barn. Om du istället vill köra förändringsdetektering från rotkomponenten (dvs. köra en fullständig förändringsdetekteringskontroll) använd ApplicationRef.tick() istället. (Jag har lagt ett anrop till ApplicationRef.tick() i kommentarer i plunkern.) Observera att ett anrop till tick() kommer att orsaka att ngDoCheck() anropas.
Svar #2
Om du inte vill hantera @angular/forms kan du bara använda ett RxJS Subject med ändringsbindningar.
vy.komponent.html
<input [ngModel]='model' (ngModelChange)='changed($event)' />
view.component.ts
import { Subject } from 'rxjs';
import { Component } from '@angular/core';
import 'rxjs/add/operator/debounceTime';
export class ViewComponent {
model: string;
modelChanged: Subject<string> = new Subject<string>();
constructor() {
this.modelChanged
.debounceTime(300) // wait 300ms after the last event before emitting last event
.distinctUntilChanged() // only emit if value is different from previous value
.subscribe(model => this.model = model);
}
changed(text: string) {
this.modelChanged.next(text);
}
}
Detta triggar ändringsdetektering. För ett sätt som inte triggar ändringsdetektering, kolla in Marks svar.
Uppdatera
.pipe(debounceTime(300), distinctUntilChanged()) behövs för rxjs 6.
Exempel :
constructor() {
this.modelChanged.pipe(
debounceTime(300),
distinctUntilChanged())
.subscribe(model => this.model = model);
}
Svar #3
Eftersom ämnet är gammalt fungerar de flesta av svaren inte på Angular 6-13 och/eller använder andra bibliotek.
Så här är en kort och enkel lösning för Angular 6+ med RxJS.
Importera det nödvändiga först:
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subject, Subscription } from 'rxjs';
import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
Implementera ngOnInit och ngOnDestroy:
export class MyComponent implements OnInit, OnDestroy {
public notesText: string;
public notesModelChanged: Subject<string> = new Subject<string>();
private notesModelChangeSubscription: Subscription
constructor() { }
ngOnInit() {
this.notesModelChangeSubscription = this.notesModelChanged
.pipe(
debounceTime(2000),
distinctUntilChanged()
)
.subscribe(newText => {
this.notesText = newText;
console.log(newText);
});
}
ngOnDestroy() {
this.notesModelChangeSubscription.unsubscribe();
}
}
Använd detta sätt:
<input [ngModel]='notesText' (ngModelChange)='notesModelChanged.next($event)' />
P.S. För mer komplexa och effektiva lösningar kanske du ändå vill kolla på andra svar.
Svar #4
Det kan implementeras som direktiv
import { Directive, Input, Output, EventEmitter, OnInit, OnDestroy } from '@angular/core';
import { NgControl } from '@angular/forms';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/distinctUntilChanged';
import { Subscription } from 'rxjs';
@Directive({
selector: '[ngModel][onDebounce]',
})
export class DebounceDirective implements OnInit, OnDestroy {
@Output()
public onDebounce = new EventEmitter<any>();
@Input('debounce')
public debounceTime: number = 300;
private isFirstChange: boolean = true;
private subscription: Subscription;
constructor(public model: NgControl) {
}
ngOnInit() {
this.subscription =
this.model.valueChanges
.debounceTime(this.debounceTime)
.distinctUntilChanged()
.subscribe(modelValue => {
if (this.isFirstChange) {
this.isFirstChange = false;
} else {
this.onDebounce.emit(modelValue);
}
});
}
ngOnDestroy() {
this.subscription.unsubscribe();
}
}
använd det som
<input [(ngModel)]="value" (onDebounce)="doSomethingWhenModelIsChanged($event)">
komponentprov
import { Component } from "@angular/core";
@Component({
selector: 'app-sample',
template: `
<input[(ngModel)]="value" (onDebounce)="doSomethingWhenModelIsChanged($event)">
<input[(ngModel)]="value" (onDebounce)="asyncDoSomethingWhenModelIsChanged($event)">
`
})
export class SampleComponent {
value: string;
doSomethingWhenModelIsChanged(value: string): void {
console.log({ value });
}
async asyncDoSomethingWhenModelIsChanged(value: string): Promise<void> {
return new Promise<void>(resolve => {
setTimeout(() => {
console.log('async', { value });
resolve();
}, 1000);
});
}
}
Svar #5
Inte direkt åtkomlig som i Angular1, men du kan enkelt leka med NgFormControl och RxJS-observabler:
<input type="text" [ngFormControl]="term"/>
this.items = this.term.valueChanges
.debounceTime(400)
.distinctUntilChanged()
.switchMap(term => this.wikipediaService.search(term));
Detta blogginlägg förklarar det tydligt: http://blog.thoughtram.io/angular/2016/01/06/taking-advantage-of-observables-in-angular2.html
Här är det för en automatisk komplettering men det fungerar i alla scenarier.
Svar #6
Du kan skapa en RxJS (v.6) Observable som gör vad du vill.
vy.komponent.html
<input type="text" (input)="onSearchChange($event.target.value)" />
vy.komponent.ts
import { Observable } from 'rxjs';
import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
export class ViewComponent {
searchChangeObserver;
onSearchChange(searchValue: string) {
if (!this.searchChangeObserver) {
new Observable(observer => {
this.searchChangeObserver = observer;
}).pipe(debounceTime(300)) // wait 300ms after the last event before emitting last event
.pipe(distinctUntilChanged()) // only emit if value is different from previous value
.subscribe(console.log);
}
this.searchChangeObserver.next(searchValue);
}
}
Svar #7
För alla som använder lodash är det mycket enkelt att debounca vilken funktion som helst:
changed = _.debounce(function() {
console.log("name changed!");
}, 400);
lägg då bara in något sådant här i din mall:
<(input)="changed($event.target.value)" />
