Je voudrais savoir si mon code pourrait créer des fuites de mémoire?
J'ai une classe de composant qui devrait afficher les objets 'Applications'. Il a des capacités de filtrage et de pagination.
J'ai créé une méthode loadAppsData () dans laquelle je m'abonne à un Observable renvoyé après une requête à un service Web.
Cette méthode est appelée au moment de l'initialisation, ngOnInit () , ou après que l'utilisateur interagit avec le champ d'entrée de filtrage ou le paginateur (voir méthode onUserInteractionsWithTree () )
Pour éviter les fuites de mémoire, j'utilise déjà
import {Component, OnInit, ViewChild, ElementRef, OnDestroy} from '@angular/core';
import {FlatTreeControl} from '@angular/cdk/tree';
import {MatPaginator} from '@angular/material';
import {DynamicFlatNode} from './dynamic-flat-node';
import {ApplicationService} from '../shared/service/application-service';
import {DataRequestOptions} from '../../shared/data/data-request-options';
import {MetaDescriptor} from '../../shared/data/meta/meta-descriptor';
import {TableDataRequestParamsService} from '../../shared/data/table-data-request-params.service';
import {ApplicationTreeDatabase} from './application-tree-database';
import {ApplicationTreeDatasource} from './application-tree-datasource';
// Observable classes and extensions.
import {BehaviorSubject, Subject, fromEvent, of, merge} from 'rxjs';
// Observable operators.
import {debounceTime, distinctUntilChanged, switchMap, takeUntil} from 'rxjs/operators';
@Component({
selector: 'app-application-tree',
templateUrl: './application-tree.component.html',
styleUrls: ['./application-tree.component.css'],
providers: [ApplicationTreeDatabase]
})
export class ApplicationTreeComponent implements OnInit, OnDestroy {
@ViewChild('appfilter') inputfilter: ElementRef;
@ViewChild(MatPaginator) paginator: MatPaginator;
readonly defaultPaginatorPageIndex = 0;
readonly defaultPaginatorPageSize = 2;
readonly defaultPaginatorPageRange = this.defaultPaginatorPageIndex + '-' + (this.defaultPaginatorPageSize - 1);
private ngUnsubscribe: Subject<void> = new Subject<void>();
// Application name filter. START
_inputFilterChange = new BehaviorSubject('');
get inputFilterValue(): string {
return this._inputFilterChange.value;
}
set inputFilterValue(inputFilterValue: string) {
this._inputFilterChange.next(inputFilterValue);
}
// Application name filter. END
treeControl: FlatTreeControl<DynamicFlatNode>;
dataSource: ApplicationTreeDatasource;
getLevel = (node: DynamicFlatNode) => node.level;
isExpandable = (node: DynamicFlatNode) => node.expandable;
hasChild = (_: number, _nodeData: DynamicFlatNode) => _nodeData.expandable;
constructor(
private applicationService: ApplicationService,
private dataRequestHelper: TableDataRequestParamsService,
private database: ApplicationTreeDatabase) {
this.treeControl = new FlatTreeControl<DynamicFlatNode>(this.getLevel, this.isExpandable);
this.dataSource = new ApplicationTreeDatasource(this.treeControl, this.paginator, database);
}
ngOnInit(): void {
fromEvent(this.inputfilter.nativeElement, 'keyup').pipe(
debounceTime(150)
, distinctUntilChanged()
, switchMap(term => of(term))
, takeUntil(this.ngUnsubscribe)
)
.subscribe(() => {
if (!this.dataSource) {
return;
}
// this.resetPaginator();
this.inputFilterValue = this.inputfilter.nativeElement.value;
});
this.loadAppsData();
this.onUserInteractionsWithTree();
}
ngOnDestroy(): void {
this.ngUnsubscribe.next(); // Unsubscribe from observables.
this.ngUnsubscribe.complete(); // Unsubscribe from ngUnsubscribe.
}
resetFilterAndTriggerChange() {
// Clear HTML filter content.
this.inputfilter.nativeElement.value = '';
// Clear filter data stream. => This will trigger database.load()
// because of Event emmited by inputFilterValueChange.
this.inputFilterValue = '';
}
buildAppDataRequestParams(): DataRequestOptions {
let range = this.dataRequestHelper.buildRequestRangeValue(this.paginator);
if (!range) { // paginator not initialized.
range = this.defaultPaginatorPageRange;
}
return new DataRequestOptions(this.inputFilterValue, 'name', range);
}
private loadAppsData() {
this.applicationService.getDataObjects(this.buildAppDataRequestParams())
.pipe(takeUntil(this.ngUnsubscribe))
.subscribe(dataAndMeta => {
// Update local Apps database.
this.database.updateApplicationData(dataAndMeta.data);
this.updatePaginator(dataAndMeta.meta);
// Inform datasource that data has changed.
this.dataSource.data = this.database.getAppsAsRootLevelNodes();
},
error => {
const errMsg = 'Echec d\'acces aux données';
throw new Error(errMsg);
}
);
}
private onUserInteractionsWithTree() {
const treeUserActionsListener = [
this._inputFilterChange,
this.paginator.page
];
// Merge the array of Observable inputs of treeUserActionsListener
// and put into the source property of a newly created Observable.
const mergeOfObservables = merge(...treeUserActionsListener);
// Create new Observable<RoleMemberClient[]> by calling the function defined below.
mergeOfObservables
.pipe(takeUntil(this.ngUnsubscribe))
.subscribe((data: any) => {
this.loadAppsData();
});
}
private updatePaginator(meta: MetaDescriptor) {
if ((meta) && (meta.isPaginatedData)) {
const contentRange = meta.contentRange;
const rangeStart = contentRange.rangeStart;
this.paginator.pageIndex = Math.floor(rangeStart / this.paginator.pageSize);
this.paginator.length = contentRange.size;
} else if (meta) {
// All data can be contained within the first table page.
this.paginator.length = meta.count;
if (this.paginator.pageIndex * this.paginator.pageSize < meta.count) {
// If last requested page do not contain data, do not reset table page index.
// The user will do it by itself.
// Otherwise reset the table page index to zero.
this.paginator.pageIndex = 0;
}
}
}
}
et
ngOnDestroy(): void {
this.ngUnsubscribe.next(); // Unsubscribe from observables.
this.ngUnsubscribe.complete(); // Unsubscribe from ngUnsubscribe.
}
Mais il me semble que je crée un nouvel objet Subscription à chaque fois que j'envoie une requête au serveur, lorsque j'appelle la méthode subscribe (). Cela peut-il créer des fuites de mémoire? Dois-je essayer de réutiliser l'objet d'abonnements?
Merci d'avance pour votre aide,
Sous le code Typescript de mon composant
.pipe(takeUntil(this.ngUnsubscribe))
4 Réponses :
Gros extrait de code, peu de choses que j'ai remarquées :), mais il est difficile de traiter toute la paix.
1.
this.data$ = merge(componentInit$, inputChanged$, nextPage$).pipe(
switchMap(() => loadData()/* should return stream, not subscription */)
)
//...
this.data$.pipe(takeUntil(/*bablba*/), map(/* transformations */)).subscribe(() => {
//actions with data
})
Il semble que vous n'en avez pas besoin pipe (takeUntil (this.ngUnsubscribe)) ici, car getDataObjects ressemble à xmlhttprequest. C'est donc terminé immédiatement.
switchMap (term => of (term)) - me semble mal. Vous créez un flux et l'aplatissez immédiatement, je suppose que l'objectif principal est de tuer le flux quand une nouvelle valeur arrive, mais cela n'a pas l'air bien.
.subscribe ((data: any) => {this.loadAppsData ();}); - Dans un abonnement, vous appelez une autre fonction qui s'abonne également à un autre flux, rend difficile à suivre.
Je rendrais votre composant plus axé sur les événements.
Divisez votre logique en fonction du moment où vous devez déclencher des données, de ce qui doit se passer lorsque de nouvelles données sont arrivées. Abonnez-vous une fois.
idée:
this.applicationService.getDataObjects(this.buildAppDataRequestParams())
.pipe(takeUntil(this.ngUnsubscribe))
Merci. Oui, je vais essayer de rendre mon code plus événementiel. Oui, je n'étais pas sûr que les observables de la requête http se terminent automatiquement.
Fuite de mémoire:
Le modèle observable est sujet à des fuites de mémoire car un abonnement qui persiste après la mort du composant (dans ce contexte) persistera pendant la durée de vie de l'application.
Par exemple: Supposons que vous ayez un composant qui s'abonne à un formControl lorsque le composant est créé mais ne ferme jamais l'abonnement, chacun lorsque vous créez le composant, vous créez un nouvel abonnement. Vous avez une fuite, vous pourriez surcharger la mémoire.
Clôture de l'abonnement:
Un abonnement prend fin lorsque l'observable se termine ou lorsque vous vous en désabonnez manuellement.
Vous avez choisi de créer un Sujet (vous l'avez appelé ngUnsubscribe - ce qui est un très mauvais nom). Vous complétez () le sujet lorsque le composant est détruit.
Cela signifie que chaque abonnement à ce sujet sera fermé lorsque le composant sera détruit.
Lorsque vous vous abonnez, vous utilisez takeUntil (ngUnsubscribe) , en fait, vous créez un miroir de votre observable d'origine et vous vous abonnez sur ce miroir.
En conséquence, lorsque votre composant est détruit, tous les abonnements effectués sur votre miroir ( ngUnsubscribe ), sont détruits. Donc non, vous n'avez pas de fuites de mémoire.
Remarques:
Étant donné qu'un abonnement est fermé lorsque l'observable est terminé, vous n'avez pas besoin de créer un miroir / désabonnement observable à partir de méthodes qui complètent votre observable, telles que HttpClient angulaire ( get < / code>, post , ...).
Vous pouvez retrouver toutes ces informations sur le lien fourni en commentaire par Alex Beugnet.
Pour une meilleure compréhension, vous pouvez vérifier:
Quelle est la différence entre complet et désabonnement?
@JoseJimRin: La différence est que l ' observable se termine alors que l' observateur / abonné se désinscrit . Si votre observable est terminée, tous les abonnements seront fermés. Si l'abonné se désabonne, l'abonnement entre observable-observer est détruit et n'existe plus en mémoire, mais l'observable peut être encore en vie et avoir des abonnements actifs avec d'autres observateurs.
Vous m'avez résolu un problème avec les observables dans un post que je voulais aider. La magie.
@JoseJimRin Je suis heureux que cela vous ait aidé! Personnellement, je ne unsubscribe () car cela signifie que vous devez détenir des instances Subscription et vous désinscrire pour chaque instance. Je préfère l'option miroir, mais j'utilise takeWhile () + un booléen (propriété de classe) au lieu de takeUntil () + subject.
Ce que vous faites est correct mais c'est le cas, mais vous devriez idéalement vous abonner en utilisant le | async à l'intérieur de votre modèle car angulaire gérera alors tous les abonnements et désabonnements pour vous. Les abonnements orphelins entraîneront des fuites de mémoire dans votre application.
Donc, dans votre composant, procédez comme suit
getDataObjects.toPromise().then(r =>
{
dataObjectSubject.next(r.map(i => // mapping here))
})
et dans votre modèle
<ng-container *ngFor="let dataObject of dataObjects | async">
// Your html markup for each dataObject here
{{dataObject | json}}
</ng-container>
Si vous voulez faire un mappage ou des événements complexes, vous devriez envisager d'utiliser BehaviourSubjects puis quelque chose du type
ngOnInit() {
this.dataObjects = this.applicationService.getDataObjects(this.buildAppDataRequestParams());
}
BehaviourSubject dans le modèle de la même manière avec le tube | async .
J'espère que cela aide
Tout d'abord,
this.ngUnsubscribe.next(); // Unsubscribe from observables.
next () PAS se désinscrire des observables. Il est utilisé pour envoyer des données via l'observable.
Pour recevoir ces données, vous devez avoir été abonné à votre sujet "ngUnsuscribe" (nom incorrect car il générerait des erreurs de compréhension) avant d'appeler la fonction suivante.
Donc, le flux correct d'un sujet est
Si vous fermez le sujet dans la ligne suivante, vous avez envoyé des données, cela pourrait générer des problèmes en raison de la nature asynchrone de la communication du sujet.
C'est discutable. Jetez un œil à l'article @benlesh, créateur de rxjs. medium.com/@benlesh/rxjs-dont-unsubscribe-6753ed4fda87
Je vais lire, merci. Dois-je utiliser complete contre le désabonnement?
Cela dépend d'un cas particulier et de préférences personnelles, j'utilise subject uniquement dans les cas extrêmes, donc je n'ai pas souvent la chance d'appeler subject.next () ou subject.complete () < / code>. Mais je peux voir que lorsque vous terminez le streaming, vous n'avez pas besoin de vous soucier du nombre d'abonnés qu'il a, lorsque vous vous désabonnez manuellement, vous devez compter tous les abonnements.
Pouvez-vous élaborer sur: "Mais il me semble que je crée un nouvel objet Subscription à chaque fois que j'envoie une requête au serveur, lorsque j'appelle la méthode subscribe ()"
Copie possible de Angular / RxJs Quand dois-je me désabonner de `Subscription` a>
Vous n'avez pas à vous désabonner de la requête http effectuée par le
HttpClientd'Angular car les Observables retournés se terminent automatiquement après avoir émis la réponse.Toutes vos réponses se trouvent dans ce lien ci-dessus.