tomar (1) vs primeiro ()

137

Eu encontrei algumas implementações de AuthGuards que usam take(1). No meu projeto, eu usei first().

Os dois funcionam da mesma maneira?

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
fonte

Respostas:

197

Operadores first()e take(1)não são os mesmos.

O first()operador assume uma predicatefunção opcional e emite uma errornotificação quando nenhum valor corresponde quando a fonte é concluída.

Por exemplo, isso emitirá um erro:

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

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

... assim como isso:

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

Enquanto isso corresponderá ao primeiro valor emitido:

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

Por outro lado, take(1)apenas pega o primeiro valor e é concluído. Nenhuma outra lógica está envolvida.

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

Então, com a fonte vazia Observable, não emitirá nenhum erro:

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

Jan 2019: atualizado para o RxJS 6

Martin
fonte
2
Apenas como nota, eu não disse isso first()e take()é o mesmo em geral, o que acho óbvio, apenas isso first()e take(1)é o mesmo. Não tenho certeza da sua resposta se você acha que ainda há uma diferença?
Günter Zöchbauer
14
@ GünterZöchbauer Na verdade, o comportamento deles é diferente. Se a fonte não emitir nada e for concluída, first()envie uma notificação de erro enquanto take(1)simplesmente não emitirá nada.
22617 martin
@martin, em alguns casos, o take (1) não emitirá nada para dizer que a depuração do código será mais difícil?
21417 Karuban
7
@ Karuban Isso realmente depende do seu caso de usuário. Se não receber nenhum valor for inesperado do que eu sugiro usar first(). Se for um estado de aplicativo válido, eu aceitaria take(1).
martin
2
Isso é semelhante ao .First()vs do .NET .FirstOrDefault()(e pense nisso também .Take(1), porque o First requer algo na coleção e gera um erro para uma coleção vazia - e ambos, FirstOrDefault()e .Take(1)permite que a coleção fique vazia e retorne nulle esvazie a coleção, respectivamente.
Simon_Weaver
44

Dica: use apenas first()se:

  • Você considera zero itens emitidos como uma condição de erro (por exemplo, concluir antes de emitir) E, se houver uma chance maior de 0% de erro, você o manipula normalmente.
  • OU Você sabe 100% que a fonte observável emitirá mais de 1 item (portanto nunca pode ser lançado) .

Se houver zero emissões e você não estiver lidando com isso explicitamente (com catchError), esse erro será propagado, possivelmente causará um problema inesperado em outro lugar e poderá ser bastante difícil de rastrear - especialmente se for de um usuário final.

Você está mais seguro usando take(1)a maior parte, desde que:

  • Você take(1)não pode emitir nada se a fonte for concluída sem emissão.
  • Você não precisa usar um predicado em linha (por exemplo, first(x => x > 10))

Nota: Você pode usar um predicado com take(1)como este: .pipe( filter(x => x > 10), take(1) ). Não há erro com isso se nada for maior que 10.

A respeito single()

Se você quiser ser ainda mais rigoroso e não permitir duas emissões, poderá usar single()quais erros se houver zero ou mais de duas emissões . Novamente, você precisaria lidar com erros nesse caso.

Dica: Singleocasionalmente pode ser útil se você quiser garantir que sua cadeia observável não esteja fazendo um trabalho extra, como chamar um serviço http duas vezes e emitir dois observáveis. Adicionar singleao final do tubo o informará se você cometeu esse erro. Estou usando-o em um 'corredor de tarefas', onde você passa uma tarefa observável que deve emitir apenas um valor; portanto, passo a resposta single(), catchError()para garantir um bom comportamento.


Por que nem sempre usar em first()vez de take(1)?

aka. Como first potencialmente pode causar mais erros?

Se você tem um observável que retira algo de um serviço e o canaliza, first()deve ficar bem a maior parte do tempo. Mas se alguém vier desativar o serviço por qualquer motivo - e alterá-lo para emitir of(null)ou NEVERentão quaisquer first()operadores a jusante começarão a gerar erros.

Agora eu percebo que pode ser exatamente o que você deseja - daí o motivo de esta ser apenas uma dica. O operador firstme atraiu porque parecia um pouco menos desajeitado do que, take(1)mas você precisa ter cuidado ao lidar com erros, se houver uma chance de a fonte não emitir. Depende inteiramente do que você está fazendo.


Se você tiver um valor padrão (constante):

Considere também .pipe(defaultIfEmpty(42), first())se você tem um valor padrão que deve ser usado se nada for emitido. Obviamente, isso não geraria um erro, porque firstsempre receberia um valor.

Observe que isso defaultIfEmptyé acionado apenas se o fluxo estiver vazio, não se o valor do que for emitido for null.

Simon_Weaver
fonte
Esteja ciente de que singlehá mais diferenças first. 1. Ele emitirá apenas o valor ativado complete. Isso significa que se o observável emitir um valor, mas nunca for concluído, o single nunca emitirá um valor. 2. Por algum motivo, se você passar uma função de filtro para a singlequal não corresponda a nada, ela emitirá um undefinedvalor se a sequência original não estiver vazia, o que não é o caso first.
Marinos Um
28

Aqui estão três observáveis A, Be Ccom diagramas de mármore para explorar a diferença entre first, takee singleoperadores:

comparação de operadores únicos vs contra vs

* Legenda : conclusão do erro de
--o-- valor
----!
----|

Brinque com ele em https://thinkrx.io/rxjs/first-vs-take-vs-single/ .

Já tendo todas as respostas, queria adicionar uma explicação mais visual

Espero que ajude alguém

kos
fonte
12

Há uma diferença realmente importante que não é mencionada em nenhum lugar.

take (1) emite 1, conclui, cancela a inscrição

first () emite 1, é concluído, mas não cancela a inscrição.

Isso significa que seu observável upstream ainda estará quente após first (), o que provavelmente não é um comportamento esperado.

UPD: Refere-se ao RxJS 5.2.0. Esse problema já pode estar corrigido.

norekhov
fonte
Acho que nenhum deles cancelou a inscrição, consulte jsbin.com/nuzulorota/1/edit?js,console .
weltschmerz
10
Sim, os dois operadores concluem a assinatura, a diferença acontece no tratamento de erros. Se esse observável não emitir valores e ainda tentar pegar o primeiro valor usando o primeiro operador, ele emitirá um erro. Se o substituirmos pelo operador take (1), embora o valor não esteja presente no fluxo quando a assinatura ocorrer, ele não emitirá um erro.
noelyahan
7
Para esclarecer: ambos cancelam a inscrição. O exemplo do @weltschmerz foi muito simplificado, não será executado até que possa cancelar a assinatura por si só. Este é um pouco mais expandida: repl.it/repls/FrayedHugeAudacity
Stephan LV
10

Parece que no RxJS 5.2.0 o .first()operador tem um bug ,

Por causa desse bug .take(1)e .first()pode se comportar bem diferente se você os estiver usando com switchMap:

Com take(1)você obterá o comportamento conforme o esperado:

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...

Mas com .first()você terá um comportamento errado:

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... 

Aqui está um link para codepen

Artem
fonte