• 8 heures
  • Moyenne

Ce cours est visible gratuitement en ligne.

course.header.alt.is_video

course.header.alt.is_certifying

J'ai tout compris !

Mis à jour le 31/03/2022

Passez d'un Observable à un autre avec les opérateurs haut niveau

Dans le chapitre précédent, vous avez utilisé des opérateurs bas niveau pour toucher directement aux émissions d'un Observable. Dans ce chapitre, vous allez découvrir les opérateurs haut niveau qui servent à gérer ce qu'on appelle des Observables haut niveau.

Observez à haut niveau

Qu'est-ce qu'un Observable haut niveau ?

En bref, il s'agit d'un Observable qui souscrit à d'autres Observables.

Mais pour quoi faire ?

Prenons un cas concret.

Imaginez une application qui permet, avec un bouton, de basculer entre deux flux de caméra. Dans cette situation, on commence avec trois Observables :

  • un Observable pour chaque caméra ;

  • l'Observable des clics sur le bouton.

Pour afficher le flux sélectionné, il faudra composer ces Observables pour créer un Observable haut niveau. On observera les clics du bouton, et selon la sélection, on ira souscrire au flux correspondant. 

Dans ce cas, on appellera le flux de clics l'Observable extérieur, et les flux des caméras les Observables intérieurs.

Un Observable haut niveau consiste donc en un Observable extérieur qui souscrit à des Observables intérieurs selon ses émissions. Quand l'Observable extérieur émet "caméra 1", il souscrit à l'Observable intérieur qui y correspond.

Ce sont les opérateurs haut niveau qui permettent ce type de souscription.

Opérez à haut niveau

Ce qui fait la différence entre ces opérateurs, c'est comment ils gèrent la situation suivante :

  • l'Observable extérieur a émis, et donc on a une première souscription à un Observable intérieur ;

  • l'Observable extérieur émet de nouveau alors que l'Observable souscrit précédemment n'a pas encore complété.

RxJS nous propose quatre stratégies, et donc quatre opérateurs, précisément pour cette situation. Voici une analogie pour expliquer ces opérateurs.

Les trains rouges et jaunes

Vous contrôlez l'intersection entre deux lignes de train : la ligne des trains rouges, et la ligne des trains jaunes. Vous avez un bouton de chaque couleur qui permet "d'appeler" un train de cette couleur. Quand vous appelez un train, il part de son dépôt et vient par sa ligne traverser votre intersection.

Vous avez donc deux Observables intérieurs ici, un pour chaque couleur. Appuyer sur le bouton rouge, c'est souscrire à l'Observable rouge, et vice versa pour le jaune. Le train qui part, qui arrive et qui passe représente les valeurs émises par l'Observable. Quand le train est passé, on considère que l'Observable a complété.

Dans votre cabine de contrôle, il y a une lumière, qui s'allume en rouge ou en jaune, qui vous dit quel train appeler. Le flux de rouge-jaune-rouge-jaune, c'est l'Observable extérieur. Pourquoi ? Parce que c'est l'Observable qui dicte à quel autre Observable vous allez souscrire : rouge ou jaune.

Situation : la lumière s'allume d'abord en jaune. Vous appuyez sur le bouton jaune, et un train jaune commence à venir. Avant l'arrivée de ce train, la lumière passe au rouge. Que faites-vous ?

mergeMap

lightObservable$.pipe(
    mergeMap(color => getTrainObservable$(color))
).subscribe();

mergeMap est l'opérateur haut niveau le plus simple : quand la lumière s'allume, on appuie sur le bouton, peu importe si le dernier train est arrivé ou non.

Cet opérateur représente donc la mise en parallèle : pour chaque émission de l'Observable extérieur,  mergeMap  souscrit à l'Observable intérieur sans se soucier si l'Observable intérieur précédemment souscrit a complété ou non.

On utilisera  mergeMap  quand l'ordre des souscriptions n'a pas besoin d'être conservé. 

concatMap

lightObservable$.pipe(
    concatMap(color => getTrainObservable$(color))
).subscribe();

Imaginons que le dernier train appelé ne soit pas encore arrivé – que l'Observable intérieur précédemment souscrit n'ait pas encore complété – et que la lumière change de couleur ;concatMap  va attendre que le train précédent arrive – que l'Observable intérieur précédent complète – avant d'appeler le train suivant. Si la lumière change de couleur plusieurs fois, on note les couleurs dans l'ordre et quand chaque train est arrivé, on appelle le suivant de la liste.

Admettons que le dernier train soit jaune : si la lumière passe de rouge à jaune et continue à changer de couleur, on note les couleurs dans l'ordre ; et quand chaque train est arrivé, on appelle le suivant de la liste.

concatMap  va donc assurer la mise en série, ce qui est important quand l'ordre des opérations doit être conservé. 

exhaustMap

lightObservable$.pipe(
    exhaustMap(color => getTrainObservable$(color))
).subscribe();

Dans ce cas, quand un train a été appelé mais n'est pas encore arrivé, on ignore totalement tout changement de la lumière. Quand le dernier train appelé passe enfin – quand le dernier Observable intérieur complète – on se remet à écouter les émissions de l'Observable extérieur. Tant qu'il y a un Observable intérieur en cours qui n'a pas complété, toute émission de l'Observable extérieur est ignorée

exhaustMap  sera utile quand un événement doit être entièrement traité avant de permettre à d'autres événements d'être émis.

switchMap

lightObservable$.pipe(
    switchMap(color => getTrainObservable$(color))
).subscribe();

Ce dernier opérateur fait en sorte que si la lumière change de couleur alors que le dernier train appelé n'est pas encore arrivé, on va annuler le train précédent et appeler le train suivant.

Quand l'Observable extérieur émet, s'il y a déjà un Observable intérieur en cours,  switchMap  va unsubscribe ("se désabonner") de lui pour subscribe au nouveau.

switchMap  servira donc quand la dernière émission de l'Observable extérieur est celle qui nous intéresse.

Pour résumer :

  • mergeMap  assure la mise en parallèle : l'Observable extérieur peut souscrire aux Observables intérieurs suivants sans attendre que les précédents soient complétés. 

  • concatMap  assure la mise en série : il attend que les Observables intérieurs complètent avant de souscrire aux suivants– même si l'Observable extérieur émet plusieurs fois. Les Observables intérieurs seront traités en séquence à la suite.

  • exhaustMap  assure le traitement complet d'une souscription avant d'observer une nouvelle émission de l'Observable extérieur. Si d’autres demandes sont faites entre temps, elles ne seront pas prises en compte. 

  • switchMap  traite la dernière demande de souscription de l’Observable extérieur et annule toute souscription précédente non-complétée.

Faites le test

Vous pouvez copier le code ci-dessous dans votre AppComponent afin de visualiser l'effet des différents opérateurs haut niveau :

import { Component, OnInit } from '@angular/core';
import { interval, of } from 'rxjs';
import { concatMap, mergeMap, delay, exhaustMap, map, switchMap, take, tap } from 'rxjs/operators';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {

  redTrainsCalled = 0;
  yellowTrainsCalled = 0;

  ngOnInit() {
    interval(500).pipe(
      take(10),
      map(value => value % 2 === 0 ? 'rouge' : 'jaune'),
      tap(color => console.log(`La lumière s'allume en %c${color}`, `color: ${this.translateColor(color)}`)),
      mergeMap(color => this.getTrainObservable$(color)),
      tap(train => console.log(`Train %c${train.color} ${train.trainIndex} arrivé !`, `font-weight: bold; color: ${this.translateColor(train.color)}`))
    ).subscribe();
  }

  getTrainObservable$(color: 'rouge' | 'jaune') {
    const isRedTrain = color === 'rouge';
    isRedTrain ? this.redTrainsCalled++ : this.yellowTrainsCalled++;
    const trainIndex = isRedTrain ? this.redTrainsCalled : this.yellowTrainsCalled;
    console.log(`Train %c${color} ${trainIndex} appelé !`, `text-decoration: underline; color: ${this.translateColor(color)}`);
    return of({ color, trainIndex }).pipe(
      delay(isRedTrain ? 5000 : 6000)
    );
  }

  translateColor(color: 'rouge' | 'jaune') {
    return color === 'rouge' ? 'red' : 'yellow';
  }
}

Si vous ouvrez la console, vous verrez une simulation de l'exemple des trains rouges et jaunes. La lumière change de couleur toutes les 500 ms, et change 10 fois en tout. Un train rouge met 5 secondes pour arriver, et un train jaune met 6 secondes.

Je vous invite à changer le  mergeMap  ligne 20 contre  concatMap  ,  exhaustMap  et  switchMap  pour explorer les différents comportements.

Implémentez l'exemple des caméras

Revenons sur l'exemple de l'application où un bouton permet de basculer entre deux flux de caméra. Pour rappel :

  • l'Observable extérieur est le flux de clics venant du bouton ;

  • les Observables intérieurs sont les flux des caméras.

Dans le code ci-dessous (qui est une simplification de la vraie implémentation), quel opérateur faudrait-il utiliser ?

const displayedStream$ = buttonClicks$.pipe(
    ******Map(chosenStream => getCameraStream$(chosenStream))
)

On peut se poser une série de questions afin de choisir le bon opérateur :

  • Est-ce que les Observables intérieurs vont compléter ?

    • Si oui, est-ce qu'il faut attendre qu'un Observable intérieur complète avant de souscrire au suivant ? Comment ça doit se comporter ?

    • Sinon, comment implémenter la fonctionnalité souhaitée ?

Vous avez trouvé ?

...

...

...

Eh oui, la réponse est bien  switchMap  . Pourquoi ? Eh bien, parce que les Observables intérieurs (les flux des caméras) ne vont pas compléter ! Du coup, qu'est-ce qui se passerait si on utilisait les autres ?

  • mergeMap  souscrirait à plusieurs flux en même temps, ce qui risque de créer des gros problèmes.

  • concatMap  attendrait qu'un flux complète avant de souscrire au suivant, mais les flux ne complètent pas.

  • exhaustMap  ignorerait carrément tous les clics à partir du premier, car il y aurait toujours un flux en cours.

Savoir si vos Observables intérieurs vont compléter (et si oui, comment et quand) vous facilitera largement le choix d'opérateur haut niveau.

En résumé

  • Un Observable haut niveau est un Observable qui souscrit à d'autres Observables.

  • L'Observable qui souscrit est appelé l'Observable extérieur, et les Observables qui sont souscrits sont appelés les Observables intérieurs.

  • Les opérateurs haut niveau servent à gérer les situations où une nouvelle émission arrive de l'Observable extérieur alors que la souscription précédente à l'Observable intérieur n'a pas encore complété :

    • mergeMap  n'attend pas qu'un Observable intérieur complète pour souscrire au suivant – il assure la mise en parallèle.

    • concatMap  attend que l'Observable intérieur complète avant de souscrire au suivant – il assure la mise en série, même lorsque l'Observable extérieur émet plusieurs fois.

    • exhaustMap  ignore toute nouvelle émission de l'Observable extérieur tant qu'il y a une souscription active à un Observable intérieur.

    • Lorsque  switchMap  reçoit une nouvelle émission de l'Observable extérieur, s'il y a une souscription active à un Observable intérieur, il l'annule et souscrit au suivant.

 Vous savez maintenant souscrire aux Observables, bravo ! Mais qu'est-ce qu'on fait quand on n'a plus besoin des émissions des Observables auxquels on a souscrit ? Suivez-moi au prochain chapitre !

Exemple de certificat de réussite
Exemple de certificat de réussite