javascript - Forhindre at ngOnChanges utløses etter å ha sendt ut en hendelse (Angular 2+)
I Angular 2+ kan egendefinert to-veis databinding oppnås ved å bruke @Input og @Output parametere. Så hvis jeg vil at en barnekomponent skal kommunisere med en tredjepartsplugin, kan jeg gjøre det som følger:
export class TestComponent implements OnInit, OnChanges {
@Input() value: number;
@Output() valueChange = new EventEmitter<number>();
ngOnInit() {
// Create an event handler which updates the parent component with the new value
// from the third party plugin.
thirdPartyPlugin.onSomeEvent(newValue => {
this.valueChange.emit(newValue);
});
}
ngOnChanges() {
// Update the third party plugin with the new value from the parent component
thirdPartyPlugin.setValue(this.value);
}
}
Og bruk det slik:
<test-component [(value)]="value"></test-component>
Etter at tredjepartspluginen utløser en hendelse for å varsle oss om en endring, oppdaterer barnekomponenten foreldrekomponenten ved å kalle this.valueChange.emit(newValue). Problemet er at ngOnChanges deretter utløses i barnekomponenten fordi verdien til foreldrekomponenten har endret seg, noe som gjør at thirdPartyPlugin.setValue(this.value) blir kalt. Men pluginen er allerede i riktig tilstand, så dette er en potensielt unødvendig/kostbar gjengivelse.
Så det jeg ofte gjør er å lage en flaggegenskap i min barnekomponent:
export class TestComponent implements OnInit, OnChanges {
ignoreModelChange = false;
ngOnInit() {
// Create an event handler which updates the parent component with the new value
// from the third party plugin.
thirdPartyPlugin.onSomeEvent(newValue => {
// Set ignoreModelChange to true if ngChanges will fire, so that we avoid an
// unnecessary (and potentially expensive) re-render.
if (this.value === newValue) {
return;
}
ignoreModelChange = true;
this.valueChange.emit(newValue);
});
}
ngOnChanges() {
if (ignoreModelChange) {
ignoreModelChange = false;
return;
}
thirdPartyPlugin.setValue(this.value);
}
}
Men dette føles som et hack.
I Angular 1 hadde direktiver som tok inn en parameter ved bruk av =-bindingen det samme eksakte problemet. I stedet ville jeg oppnådd egendefinert toveis databinding ved å kreve ngModelController, som ikke forårsaket en ny-rendering etter en modelloppdatering:
// Update the parent model with the new value from the third party plugin. After the model
// is updated, $render will not fire, so we don't have to worry about a re-render.
thirdPartyPlugin.onSomeEvent(function (newValue) {
scope.$apply(function () {
ngModelCtrl.$setViewValue(newValue);
});
});
// Update the third party plugin with the new value from the parent model. This will only
// fire if the parent scope changed the model (not when we call $setViewValue).
ngModelCtrl.$render = function () {
thirdPartyPlugin.setValue(ngModelCtrl.$viewValue);
};
Dette fungerte, men ngModelController ser virkelig ut til å være designet for formelementer (det har innebygd validering osv.). Så det føltes litt rart å bruke det i egendefinerte direktiver som ikke er formelementer.
Spørsmål: Finnes det en beste praksis i Angular 2+ for å implementere egendefinert toveis databinding i en barnekomponent, som ikke utløser ngOnChanges i barnekomponenten etter oppdatering av foreldrekomponenten ved bruk av EventEmitter? Eller bør jeg integrere med ngModel akkurat som jeg gjorde i Angular 1, selv om barnekomponenten min ikke er et formelement?
Takk på forhånd!
Oppdatering: Jeg sjekket ut Alt du trenger å vite om endringsdeteksjon i Angular foreslått av @Maximus i kommentarene. Det ser ut til at detach-metoden på ChangeDetectorRef vil forhindre at noen bindings i malen blir oppdatert, noe som kan hjelpe med ytelsen hvis det er din situasjon. Men det hindrer ikke ngOnChanges fra å bli kalt:
thirdPartyPlugin.onSomeEvent(newValue => {
// ngOnChanges will still fire after calling emit
this.changeDetectorRef.detach();
this.valueChange.emit(newValue);
});
Så langt har jeg ikke funnet en måte å oppnå dette på ved bruk av Angulars endringsdeteksjon (men jeg lærte mye i prosessen!).
Jeg endte opp med å prøve dette med ngModel og ControlValueAccessor. Dette ser ut til å oppnå det jeg trenger siden det oppfører seg som ngModelController i Angular 1:
export class TestComponentUsingNgModel implements ControlValueAccessor, OnInit {
value: number;
// Angular will pass us this function to invoke when we change the model
onChange = (fn: any) => { };
ngOnInit() {
thirdPartyPlugin.onSomeEvent(newValue => {
this.value = newValue;
// Tell Angular to update the parent component with the new value from the third
// party plugin
this.onChange(newValue);
});
}
// Update the third party plugin with the new value from the parent component. This
// will only fire if the parent component changed the model (not when we call
// this.onChange).
writeValue(newValue: number) {
this.value = newValue;
thirdPartyPlugin.setValue(this.value);
}
registerOnChange(fn: any) {
this.onChange = fn;
}
}
Og bruk det slik:
<test-component-using-ng-model [(ngModel)]="value"></test-component-using-ng-model>
Men igjen, hvis den tilpassede komponenten ikke er et formelement, virker det litt merkelig å bruke ngModel.
Svar #1
Støtte også på dette problemet (eller i det minste noe veldig liknende).
Jeg endte opp med å bruke den kjipe metoden du diskuterte ovenfor, men med en liten modifikasjon, jeg brukte setTimeout for å tilbakestille tilstanden i tilfelle.
(For meg personlig var ngOnChanges hovedsakelig problematisk hvis man brukte toveis binding, så setTimeout forhindrer en hengende disableOnChanges hvis man IKKE bruker toveis binding).
changePage(newPage: number) {
this.page = newPage;
updateOtherUiVars();
this.disableOnChanges = true;
this.pageChange.emit(this.page);
setTimeout(() => this.disableOnChanges = false, 0);
}
ngOnChanges(changes: any) {
if (this.disableOnChanges) {
this.disableOnChanges = false;
return;
}
updateOtherUiVars();
}
Svar #2
Dette er nøyaktig intensjonen med Angular, og det er noe du bør prøve å arbeide med i stedet for mot. Endringsdeteksjon fungerer ved at komponenter oppdager endringer i sine malbindinger og sprer dem ned komponenttreet. Hvis du kan designe applikasjonen din på en slik måte at du er avhengig av immutabiliteten til komponentens input, kan du kontrollere dette manuelt ved å sette @Component({changeDetection:ChangeDetectionStrategy.OnPush}), som vil teste referanser for å avgjøre om endringsdeteksjon skal fortsette på barnekomponenter.
Så, med det sagt, er min erfaring at wrappers for tredjeparts plugins kanskje ikke effektivt håndterer og utnytter denne typen strategi på en passende måte. Du bør forsøke å bruke kunnskapen ovenfor, sammen med gode designvalg som separasjon av presentasjons- og containerkomponenter, for å utnytte deteksjonsstrategien og oppnå god ytelse.
Du kan også sende changes: SimpleChanges til ngOnInit(changes: SimpleChanges) og inspisere objektet for å lære mer om datatilstrømningen din.
