Take (1) vs First ()

146

Znalazłem kilka implementacji, AuthGuardktóre wykorzystują take(1). W moim projekcie użyłem first().

Czy oba działają w ten sam sposób?

import 'rxjs/add/operator/map';
import 'rxjs/add/operator/first';
import { Observable } from 'rxjs/Observable';

import { Injectable } from '@angular/core';
import { CanActivate, Router, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { AngularFire } from 'angularfire2';

@Injectable()
export class AuthGuard implements CanActivate {

    constructor(private angularFire: AngularFire, private router: Router) { }

    canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | boolean {
        return this.angularFire.auth.map(
            (auth) =>  {
                if (auth) {
                    this.router.navigate(['/dashboard']);
                    return false;
                } else {
                    return true;
                }
            }
        ).first(); // Just change this to .take(1)
    }
}
Karuban
źródło

Odpowiedzi:

206

Operatorzy first()i take(1)nie są tacy sami.

first()Operator bierze opcjonalną predicatefunkcję i emituje errorpowiadomienie, gdy żadna wartość dopasowane gdy źródło zakończone.

Na przykład spowoduje to wyświetlenie błędu:

import { EMPTY, range } from 'rxjs';
import { first, take } from 'rxjs/operators';

EMPTY.pipe(
  first(),
).subscribe(console.log, err => console.log('Error', err));

... tak dobrze jak to:

range(1, 5).pipe(
  first(val => val > 6),
).subscribe(console.log, err => console.log('Error', err));

Chociaż będzie to zgodne z pierwszą wyemitowaną wartością:

range(1, 5).pipe(
  first(),
).subscribe(console.log, err => console.log('Error', err));

Z drugiej strony take(1)po prostu przyjmuje pierwszą wartość i kończy. Nie ma dalszej logiki.

range(1, 5).pipe(
  take(1),
).subscribe(console.log, err => console.log('Error', err));

Wtedy z pustym źródłem Observable nie wyemituje żadnego błędu:

EMPTY.pipe(
  take(1),
).subscribe(console.log, err => console.log('Error', err));

Sty 2019: Zaktualizowano dla RxJS 6

jaskółka oknówka
źródło
2
Tak jak uwaga, tego nie powiedziałem first()i take()są w ogóle takie same, co myślę, że jest oczywiste, tylko to first()i take(1)są takie same. Nie jestem pewien z Twojej odpowiedzi, czy uważasz, że nadal istnieje różnica?
Günter Zöchbauer
15
@ GünterZöchbauer Właściwie ich zachowanie jest inne. Jeśli źródło niczego nie emituje i kończy, first()wyślij powiadomienie o błędzie, podczas gdy take(1)po prostu nic nie wyemituje.
martin
@martin, w niektórych przypadkach take (1) nie wyemituje niczego, co oznacza, e debugowanie kodu będzie trudniejsze?
Karuban
7
@Karuban To naprawdę zależy od twojego przypadku. Jeśli nie otrzymanie żadnej wartości jest nieoczekiwane, sugerowałbym użyć first(). Jeśli jest to prawidłowy stan aplikacji, wybrałbym take(1).
martin
2
Jest to podobne do .NET's .First()vs .FirstOrDefault()(i myślę o tym również .Take(1)w tym, że First wymaga czegoś w kolekcji i daje błąd dla pustej kolekcji - FirstOrDefault()i .Take(1)pozwala na to, aby kolekcja była pusta i nullodpowiednio
zwracała
50

Wskazówka: używaj tylko, first()jeśli:

  • Uważasz, że zero wyemitowanych elementów jest warunkiem błędu (np. Ukończenie przed emisją) ORAZ jeśli istnieje większe niż 0% prawdopodobieństwo błędu, radzisz sobie z nim wdzięcznie
  • LUB Wiesz w 100%, że źródło, które można zaobserwować, wyemituje 1+ przedmiotów (więc nigdy nie możesz rzucać) .

Jeśli emisje są zerowe i nie obsługujesz ich bezpośrednio (z catchError), ten błąd zostanie propagowany, prawdopodobnie spowoduje nieoczekiwany problem w innym miejscu i może być dość trudny do wyśledzenia - zwłaszcza jeśli pochodzi od użytkownika końcowego.

W większości przypadków bezpieczniej jest używać, take(1)pod warunkiem, że:

  • Nie musisz take(1)nic emitować, jeśli źródło zakończy się bez emisji.
  • Nie musisz używać predykatu wbudowanego (np. first(x => x > 10))

Uwaga: Można użyć predykatu z take(1)tak: .pipe( filter(x => x > 10), take(1) ). Nie ma błędu, jeśli nic nie jest większe niż 10.

Co powiesz na single()

Jeśli chcesz być jeszcze bardziej rygorystyczny i nie zezwalać na dwie emisje, możesz użyć single()których błędów, jeśli są emisje zerowe lub 2+ . W takim przypadku znowu będziesz musiał obsłużyć błędy.

Wskazówka: Singleczasami może się przydać, jeśli chcesz mieć pewność, że twój obserwowalny łańcuch nie wykonuje dodatkowej pracy, takiej jak dwukrotne wywołanie usługi http i emisja dwóch obserwowalnych. Dodanie singlena końcu rury poinformuje Cię, jeśli popełniłeś taki błąd. Używam go w 'module uruchamiającym zadania', w którym przekazujesz obserwowalne zadanie, które powinno emitować tylko jedną wartość, więc przekazuję odpowiedź, single(), catchError()aby zagwarantować dobre zachowanie.


Dlaczego nie zawsze używać first()zamiast take(1)?

znany jako. Jak first potencjalnie może powodować więcej błędów?

Jeśli masz obiekt obserwowalny, który pobiera coś z usługi, a następnie przesyła go przez kanał, przez first()większość czasu powinno być dobrze. Ale jeśli ktoś przyjdzie, aby wyłączyć usługę z jakiegokolwiek powodu - i zmieni ją na emisję, of(null)albo NEVERkażdy dalszy first()operator zacznie rzucać błędy.

Teraz zdaję sobie sprawę, że może to być dokładnie to, czego chcesz - dlatego to tylko wskazówka. Operator firstzaapelował do mnie, ponieważ brzmiał nieco mniej „niezgrabnie” niż, take(1)ale trzeba uważać z obsługą błędów, jeśli jest szansa, że ​​źródło nie emituje. Zależy to całkowicie od tego, co robisz.


Jeśli masz wartość domyślną (stałą):

Zastanów się również, .pipe(defaultIfEmpty(42), first())czy masz wartość domyślną, której należy użyć, jeśli nic nie jest emitowane. To oczywiście nie spowodowałoby błędu, ponieważ firstzawsze otrzymywałoby wartość.

Zauważ, że defaultIfEmptyjest wyzwalane tylko wtedy, gdy strumień jest pusty, a nie, jeśli wartość tego, co jest emitowane, wynosi null.

Simon_Weaver
źródło
Pamiętaj, że singlema więcej różnic first. 1. Emituje tylko wartość on complete. Oznacza to, że jeśli obserwowalne emitują wartość, ale nigdy nie kończą, to single nigdy nie wyemitują wartości. 2. Z jakiegoś powodu, jeśli przekażesz funkcję filtrującą, do singlektórej nic nie pasuje, wyemituje undefinedwartość, jeśli oryginalna sekwencja nie jest pusta, co nie ma miejsca w przypadku first.
Marinos An
32

Oto trzy obserwabli A, Boraz Cze schematami marmurowe do zbadania różnicy między first, takei singleoperatorzy:

porównanie pierwsze vs ujęcie vs porównanie pojedynczych operatorów

* Legenda :
--o-- wartość uzupełnienia
----! błędu
----|

Graj z nim na https://thinkrx.io/rxjs/first-vs-take-vs-single/ .

Mając już wszystkie odpowiedzi, chciałem dodać bardziej wizualne wyjaśnienie

Mam nadzieję, że to komuś pomoże

kos
źródło
13

Jest jedna naprawdę ważna różnica, o której nigdzie nie wspomniano.

take (1) emituje 1, kończy, anuluje subskrypcję

first () emituje 1, kończy, ale nie anuluje subskrypcji.

Oznacza to, że twój obserwowalny element wyjściowy będzie nadal gorący po first (), co prawdopodobnie nie jest oczekiwanym zachowaniem.

UPD: Dotyczy to RxJS 5.2.0. Ten problem może już zostać rozwiązany.

norekhov
źródło
Nie sądzę, żeby któryś z nich zrezygnował z subskrypcji, zobacz jsbin.com/nuzulorota/1/edit?js,console .
weltschmerz
11
Tak, obaj operatorzy kończą subskrypcję, różnica polega na obsłudze błędów. Jeśli ta obserwowalna nie wyemituje wartości i nadal będzie próbowała pobrać pierwszą wartość za pomocą pierwszego operatora, zgłosi błąd. Jeśli zastąpimy go operatorem take (1), mimo że w strumieniu nie ma wartości, gdy następuje subskrypcja, nie powoduje to błędu.
noelyahan
8
Wyjaśnienie: obie rezygnują z subskrypcji. Przykład z @weltschmerz był zbyt uproszczony, nie działa, dopóki nie może sam się wypisać. Ten jest nieco bardziej rozbudowany: repl.it/repls/FrayedHugeAudacity
Stephan LV
10

Wygląda na to, że w RxJS 5.2.0 .first()operator ma błąd ,

Z powodu tego błędu .take(1)i .first()mogą zachowywać się zupełnie inaczej, jeśli używasz ich z switchMap:

Dzięki take(1)temu uzyskasz oczekiwane zachowanie:

var x = Rx.Observable.interval(1000)
   .do( x=> console.log("One"))
   .take(1)
   .switchMap(x => Rx.Observable.interval(1000))
   .do( x=> console.log("Two"))
   .subscribe((x) => {})

// In the console you will see:
// One
// Two
// Two
// Two
// Two
// etc...

Ale z .first()tobą uzyskasz złe zachowanie:

var x = Rx.Observable.interval(1000)
  .do( x=> console.log("One"))
  .first()
  .switchMap(x => Rx.Observable.interval(1000))
  .do( x=> console.log("Two"))
  .subscribe((x) => {})

// In console you will see:
// One
// One
// Two
// One
// Two
// One
// etc... 

Oto link do codepen

Artem
źródło
0

Okazuje się, że istnieje bardzo ważna różnica między tymi dwiema metodami: first () wyemituje błąd, jeśli strumień zakończy się przed wyemitowaniem wartości. Lub, jeśli podałeś predykat (i.e. first(value => value === 'foo')), wyemituje błąd, jeśli strumień zakończy się przed wyemitowaniem wartości, która przekazuje predykat.

Z drugiej strony take (1) będzie z radością kontynuowany, jeśli wartość nigdy nie zostanie wyemitowana ze strumienia. Oto prosty przykład:

const subject$ = new Subject();

// logs "no elements in sequence" when the subject completes
subject$.first().subscribe(null, (err) => console.log(err.message));

// never does anything
subject$.take(1).subscribe(console.log);

subject$.complete();

Inny przykład, używając predykatu:

const observable$ = of(1, 2, 3);

// logs "no elements in sequence" when the observable completes
observable$
 .first((value) => value > 5)
 .subscribe(null, (err) => console.log(err.message));

// the above can also be written like this, and will never do
// anything because the filter predicate will never return true
observable$
 .filter((value) => value > 5);
 .take(1)
 .subscribe(console.log);

Jako nowicjusza w RxJS, to zachowanie było dla mnie bardzo zagmatwane, chociaż było to moja wina, ponieważ poczyniłem nieprawidłowe założenia. Gdybym zadał sobie trud sprawdzania dokumentów, zobaczyłbym, że zachowanie jest wyraźnie udokumentowane :

Zgłasza błąd, jeśli defaultValuenie został podany i nie znaleziono pasującego elementu.

Powodem, dla którego spotykam się z tym tak często, jest dość powszechny wzorzec Angular 2, w którym obserwowalne są czyszczone ręcznie podczas OnDestroyhaka cyklu życia:

class MyComponent implements OnInit, OnDestroy {
  private stream$: Subject = someDelayedStream();
  private destroy$ = new Subject();

  ngOnInit() {
    this.stream$
      .takeUntil(this.destroy$)
      .first()
      .subscribe(doSomething);
  }

  ngOnDestroy() {
    this.destroy$.next(true);
  }
}

Kod początkowo wygląda nieszkodliwie, ale pojawiają się problemy, gdy komponent, który został wcześniej zniszczony, stream$może wyemitować wartość. Ponieważ używam first(), po zniszczeniu składnika jest zgłaszany błąd. Zwykle subskrybuję strumień tylko w celu uzyskania wartości, która ma być używana w składniku, więc nie obchodzi mnie, czy komponent zostanie zniszczony przed emisją strumienia. Z tego powodu zacząłem używać take(1)w prawie wszystkich miejscach, w których wcześniej bym używał first().

filter(fn).take(1)jest nieco bardziej rozwlekły niż first(fn), ale w większości przypadków wolę nieco więcej szczegółowości niż obsługa błędów, które ostatecznie nie mają wpływu na aplikację.

Warto również zwrócić uwagę: to samo dotyczy last()i takeLast(1).

odniesienie

Kamran Khatti
źródło