Quand dois-je créer un nouvel abonnement pour un effet secondaire spécifique?

10

La semaine dernière, j'ai répondu à une question RxJS où j'ai eu une discussion avec un autre membre de la communauté sur: "Dois-je créer un abonnement pour chaque effet secondaire spécifique ou dois-je essayer de minimiser les abonnements en général?" Je veux savoir quelle méthologie utiliser en termes d'approche applicative réactive complète ou quand passer de l'un à l'autre. Cela m'aidera et peut-être d'autres à éviter les discussions sans crainte.

Informations de configuration

  • Tous les exemples sont en TypeScript
  • Pour mieux se concentrer sur la question, pas d'utilisation des cycles de vie / constructeurs pour les abonnements et pour garder le cadre sans rapport
    • Imaginez: les abonnements sont ajoutés dans constructeur / cycle de vie init
    • Imaginez: la désinscription se fait dans le cycle de vie détruire

Qu'est-ce qu'un effet secondaire (échantillon angulaire)

  • Mise à jour / entrée dans l'interface utilisateur (par exemple value$ | async)
  • Sortie / amont d'un composant (par exemple @Output event = event$)
  • Interaction entre différents services sur différentes hiérarchies

Cas d'utilisation exemplaire:

  • Deux fonctions: foo: () => void; bar: (arg: any) => void
  • Deux sources observables: http$: Observable<any>; click$: Observable<void>
  • fooest appelé après http$avoir émis et n'a besoin d'aucune valeur
  • barest appelé après l' click$émission, mais a besoin de la valeur actuelle dehttp$

Cas: Créez un abonnement pour chaque effet secondaire spécifique

const foo$ = http$.pipe(
  mapTo(void 0)
);

const bar$ = http$.pipe(
  switchMap(httpValue => click$.pipe(
    mapTo(httpValue)
  )
);

foo$.subscribe(foo);
bar$.subscribe(bar);

Cas: minimiser les abonnements en général

http$.pipe(
  tap(() => foo()),
  switchMap(httpValue => click$.pipe(
    mapTo(httpValue )
  )
).subscribe(bar);

Ma propre opinion en bref

Je peux comprendre le fait que les abonnements rendent les paysages Rx plus complexes au début, car vous devez penser à la façon dont les abonnés devraient affecter la pipe ou non par exemple (partagez votre observable ou non). Mais plus vous séparez votre code (plus vous vous concentrez: que se passe-t-il quand), plus il est facile de maintenir (tester, déboguer, mettre à jour) votre code à l'avenir. Dans cet esprit, je crée toujours une seule source observable et un seul abonnement pour tout effet secondaire dans mon code. Si deux ou plusieurs effets secondaires que j'ai sont déclenchés par la même source observable, je partage mon observable et je souscris à chaque effet secondaire individuellement, car il peut avoir des cycles de vie différents.

Jonathan Stellwag
la source

Réponses:

6

RxJS est une ressource précieuse pour la gestion des opérations asynchrones et doit être utilisé pour simplifier votre code (notamment en réduisant le nombre d'abonnements) lorsque cela est possible. De même, un observable ne doit pas être automatiquement suivi d'un abonnement à cet observable, si RxJS fournit une solution qui peut réduire le nombre total d'abonnements dans votre application.

Cependant, il existe des situations où il peut être avantageux de créer un abonnement qui n'est pas strictement «nécessaire»:

Un exemple d'exception - réutilisation d'observables dans un seul modèle

En regardant votre premier exemple:

// Component:

this.value$ = this.store$.pipe(select(selectValue));

// Template:

<div>{{value$ | async}}</div>

Si la valeur $ n'est utilisée qu'une seule fois dans un modèle, je profiterais du canal asynchrone et de ses avantages pour l'économie de code et la désinscription automatique. Cependant, selon cette réponse , plusieurs références à la même variable asynchrone dans un modèle doivent être évitées, par exemple:

// It works, but don't do this...

<ul *ngIf="value$ | async">
    <li *ngFor="let val of value$ | async">{{val}}</li>
</ul>

Dans cette situation, je créerais plutôt un abonnement séparé et l'utiliserais pour mettre à jour une variable non asynchrone dans mon composant:

// Component

valueSub: Subscription;
value: number[];

ngOnInit() {
    this.valueSub = this.store$.pipe(select(selectValue)).subscribe(response => this.value = response);
}

ngOnDestroy() {
    this.valueSub.unsubscribe();
}

// Template

<ul *ngIf="value">
    <li *ngFor="let val of value">{{val}}</li>
</ul>

Techniquement, il est possible d'obtenir le même résultat sans valueSub, mais les exigences de l'application signifient que c'est le bon choix.

Considérer le rôle et la durée de vie d'un observable avant de décider de s'abonner

Si deux ou plusieurs observables ne sont utiles que lorsqu'ils sont pris ensemble, les opérateurs RxJS appropriés doivent être utilisés pour les combiner en un seul abonnement.

De même, si first () est utilisé pour filtrer tout sauf la première émission d'un observable, je pense qu'il y a plus de raisons d'être économique avec votre code et d'éviter les abonnements `` supplémentaires '' que pour un observable qui a un rôle continu dans la session.

Lorsque l'un des observables individuels est utile indépendamment des autres, la flexibilité et la clarté des abonnements distincts peuvent être envisagées. Mais selon ma déclaration initiale, un abonnement ne devrait pas être créé automatiquement pour chaque observable, sauf s'il y a une raison claire de le faire.

Concernant les désabonnements:

Un point contre les abonnements supplémentaires est que davantage de désabonnements sont nécessaires. Comme vous l'avez dit, nous aimerions supposer que tous les désabonnements nécessaires sont appliqués sur Destroy, mais la vraie vie ne se passe pas toujours aussi bien! Encore une fois, RxJS fournit des outils utiles (par exemple first () ) pour rationaliser ce processus, ce qui simplifie le code et réduit le risque de fuites de mémoire. Cet article fournit des informations supplémentaires et des exemples pertinents, qui peuvent être utiles.

Préférence personnelle / verbosité vs lacune:

Tenez compte de vos propres préférences. Je ne veux pas m'égarer vers une discussion générale sur la verbosité du code, mais l'objectif devrait être de trouver le bon équilibre entre trop de «bruit» et rendre votre code trop cryptique. Cela pourrait valoir le coup d'oeil .

Matt Saunders
la source
Merci d'abord pour votre réponse détaillée! Concernant # 1: de mon point de vue, le canal asynchrone est également un abonnement / effet secondaire, juste qu'il est masqué à l'intérieur d'une directive. En ce qui concerne le # 2, pouvez-vous ajouter un exemple de code, je ne comprends pas exactement ce que vous me dites. Concernant les désabonnements: je ne l'ai jamais eu avant que les abonnements doivent être désabonnés à un autre endroit que le ngOnDestroy. First () ne gère pas vraiment le désabonnement pour vous par défaut: 0 émet = abonnement ouvert, bien que le composant soit détruit.
Jonathan Stellwag
1
Le point 2 consistait vraiment à considérer le rôle de chaque observable lors de la décision de créer ou non un abonnement plutôt que de suivre automatiquement chaque observable avec un abonnement. Sur ce point, je suggérerais de regarder medium.com/@benlesh/rxjs-dont-unsubscribe-6753ed4fda87 , qui dit: "Garder trop d'objets d'abonnement est un signe que vous gérez impérativement vos abonnements et que vous n'en profitez pas de la puissance de Rx. "
Matt Saunders
1
Merci à @JonathanStellwag. Merci également pour les rappels info RE - dans vos applications, vous désinscrivez-vous explicitement de chaque abonnement (même lorsque first () est utilisé, par exemple), pour éviter cela?
Matt Saunders
1
Oui. Dans le projet actuel, nous avons minimisé environ 30% de tous les nœuds qui traînent en se désabonnant toujours.
Jonathan Stellwag
2
Ma préférence pour la désinscription est takeUntilassociée à une fonction appelée depuis ngOnDestroy. Il est une doublure qui l' ajoute à la conduite: takeUntil(componentDestroyed(this)). stackoverflow.com/a/60223749/5367916
Kurt Hamilton
2

si l'optimisation des abonnements est votre fin de partie, pourquoi ne pas aller à l'extrême logique et suivre simplement ce schéma général:

 const obs1$ = src1$.pipe(tap(effect1))
 const obs2$ = src2$pipe(tap(effect2))
 merge(obs1$, obs2$).subscribe()

L'exécution exclusive des effets secondaires dans le robinet et l'activation avec la fusion signifie que vous n'avez qu'un seul abonnement.

Une raison de ne pas le faire est que vous neutralisez une grande partie de ce qui rend RxJS utile. Il s'agit de la possibilité de composer des flux observables et de vous abonner / vous désabonner des flux selon les besoins.

Je dirais que vos observables devraient être logiquement composés, et non pollués ou confondus au nom de la réduction des abonnements. L'effet foo doit-il logiquement être couplé à l'effet bar? L'un nécessite-t-il l'autre? Pourrai-je jamais ne pas vouloir déclencher foo lorsque http $ émet? Suis-je en train de créer un couplage inutile entre des fonctions indépendantes? Ce sont toutes des raisons pour éviter de les mettre dans un seul flux.

Tout cela ne prend même pas en compte la gestion des erreurs qui est plus facile à gérer avec plusieurs abonnements IMO

bryan60
la source
Merci pour votre réponse. Je suis désolé de ne pouvoir accepter qu'une seule réponse. Votre réponse est la même que celle écrite par @Matt Saunders. C'est juste un autre point de vue. Grâce aux efforts de Matt, je lui ai donné l'acceptation. J'espère que vous pouvez me pardonner :)
Jonathan Stellwag