Angular - * ngIf vs chamadas de função simples no modelo

14

Desculpe se isso já foi respondido aqui, mas não encontrei nenhuma correspondência para o nosso cenário específico, então aqui vai!

Tivemos uma discussão em nossa equipe de desenvolvimento, sobre chamadas de função em modelos angulares. Agora, como regra geral, concordamos que você não deve fazer isso. No entanto, tentamos discutir quando pode estar tudo bem. Deixe-me dar um cenário.

Digamos que temos um bloco de modelo envolvido em um ngIf, que verifica vários parâmetros, como aqui:

<ng-template *ngIf="user && user.name && isAuthorized">
 ...
</ng-template>

Haveria uma diferença significativa no desempenho em comparação com algo como isto:

Modelo:

<ng-template *ngIf="userCheck()">
 ...
</ng-template>

Texto datilografado:

userCheck(): boolean {
  return this.user && this.user.name && this.isAuthorized;
}

Então, para resumir a pergunta, a última opção teria algum custo de desempenho significativo?

Preferimos usar a segunda abordagem, em situações em que precisamos verificar mais de duas condições, mas muitos artigos online dizem que as chamadas de funções SEMPRE são ruins nos modelos, mas será realmente um problema nesse caso?

Jesper
fonte
7
Não, não seria. E também é mais limpo, pois torna o modelo mais legível, a condição mais facilmente testável e reutilizável, e você tem mais ferramentas à sua disposição (toda a linguagem TypeScript) para torná-lo o mais legível e eficiente possível. Eu escolheria um nome muito mais claro que "userCheck".
JB Nizet 17/01
Muito obrigado pela sua contribuição :)
Jesper

Respostas:

8

Eu também tentei evitar chamadas de funções nos modelos o máximo possível, mas sua pergunta me inspirou a fazer uma pesquisa rápida:

Adicionei outro caso com userCheck()resultados de cache

*ngIf="isUserChecked"

...
// .ts
isUserChecked = this.userCheck()

Preparou uma demonstração aqui: https://stackblitz.com/edit/angular-9qgsm9

Surpreendentemente, parece que não há diferença entre

*ngIf="user && user.name && isAuthorized"

E

*ngIf="userCheck()"

...
// .ts
userCheck(): boolean {
  return this.user && this.user.name && this.isAuthorized;
}

E

*ngIf="isUserChecked"

...
// .ts
isUserChecked = this.userCheck()

Parece válido para uma verificação de propriedade simples, mas definitivamente haverá uma diferença se houver alguma asyncação, getters que estão esperando por alguma API, por exemplo.

qiAlex
fonte
10

Esta é uma resposta bastante opinativa.

O uso de funções como essa é perfeitamente aceitável. Isso tornará os modelos muito mais claros e não causará nenhuma sobrecarga significativa. Como o JB disse antes, ele também estabelecerá uma base muito melhor para testes de unidade.

Também acho que qualquer expressão que você tenha no seu modelo será avaliada como uma função pelo mecanismo de detecção de alterações, portanto, não importa se você a possui no modelo ou na lógica do componente.

Apenas mantenha a lógica dentro da função no mínimo. Se você estiver no entanto cauteloso sobre qualquer impacto no desempenho essa função um pode ter, eu recomendo fortemente que você colocar o seu ChangeDetectionStrategypara OnPush, que é considerada a melhor prática de qualquer maneira. Com isso, a função não será chamada a cada ciclo, somente quando uma Inputalteração, algum evento ocorrer dentro do modelo, etc.

(usando etc, porque não conheço mais o outro motivo) .


Pessoalmente, novamente, acho que é ainda melhor usar o padrão Observables; você pode usar o asyncpipe e, somente quando um novo valor é emitido, o modelo é reavaliado:

userIsAuthorized$ = combineLatest([
  this.user$,
  this.isAuthorized$
]).pipe(
  map(([ user, authorized ]) => !!user && !!user.name && authorized),
  shareReplay({ refCount: true, bufferSize: 1 })
);

Você pode apenas usar no modelo como este:

<ng-template *ngIf="userIsAuthorized$ | async">
 ...
</ng-template>

Ainda outra opção seria usar ngOnChanges, se todas as variáveis ​​dependentes do componente forem Entradas, e você tiver muita lógica para calcular uma determinada variável de modelo (que não é o caso que você mostrou):

export class UserComponent implements ngOnChanges {
  userIsAuthorized: boolean = false;

  @Input()
  user?: any;

  @Input()
  isAuthorized?: boolean;

  ngOnChanges(changes: SimpleChanges): void {
    if (changes.user || changes.isAuthorized) {
      this.userIsAuthorized = this.userCheck();
    }
  }

  userCheck(): boolean {
    return this.user && this.user.name && this.isAuthorized || false;
  }
}

Que você pode usar no seu modelo assim:

<ng-template *ngIf="userIsAuthorized">
 ...
</ng-template>
Poul Kruijt
fonte
Obrigado pela sua resposta, muito perspicaz. Porém, para o nosso caso específico, alterar a estratégia de detecção não é uma opção, pois o componente em questão executa uma solicitação get e, portanto, a alteração não está relacionada a uma entrada específica, mas à solicitação get. No entanto, essa é uma informação muito útil para o desenvolvimento de futuros componentes onde a mudança depende de variáveis ​​de entrada.
Jesper
11
@ Jesper, se o componente executar uma solicitação get, você já terá um Observablefluxo, o que o tornará um candidato perfeito para a segunda opção que mostrei. De qualquer forma, fico feliz por poder lhe dar algumas idéias
Poul Kruijt 17/01
6

Não é recomendado por muitos motivos o principal:

Para determinar se o userCheck () precisa ser renderizado novamente, o Angular precisa executar a expressão userCheck () para verificar se o valor de retorno foi alterado.

Como o Angular não pode prever se o valor de retorno de userCheck () foi alterado, ele precisa executar a função sempre que a detecção de alterações for executada.

Portanto, se a detecção de alterações for executada 300 vezes, a função será chamada 300 vezes, mesmo que seu valor de retorno nunca seja alterado.

Explicação estendida e mais problemas https://medium.com/showpad-engineering/why-you-should-never-use-function-calls-in-angular-template-expressions-e1a50f9c0496

O problema ocorre quando se o seu componente é grande e participa de muitos eventos de mudança, se o componente for pequeno e apenas participar de alguns eventos não deve ser um problema.

Exemplo com observáveis

user$;
isAuth$
userCheck$;

userCheck$ = user$.pipe(
switchMap((user) => {
    return forkJoin([of(user), isAuth$]);
 }
)
.map(([user, isAuthenticated])=>{
   if(user && user.name && isAuthenticated){
     return true;
   } else {
     return false;
   }
})
);

Então você pode usá-lo como observável com o canal assíncrono no seu código.

anthony willis muñoz
fonte
2
Olá, gostaria apenas de salientar que a sugestão de usar uma variável é seriamente enganosa. A variável não será atualizada com valor quando qualquer um dos valores combinados for alterado
nsndvd
11
E se a expressão estiver diretamente no modelo ou retornada por uma função, ela deverá ser avaliada a cada detecção de alteração.
JB Nizet 17/01
Sim, sua verdadeira pena será editada por não fazer más práticas
anthony willis muñoz
@ anthonywillismuñoz Então, como você abordaria uma situação como essa? Basta viver com as várias condições difíceis de ler no * ngIf?
Jesper
11
isso depende da sua situação, você tem algumas opções no post médio. Mas acho que você está usando observáveis. Editará a postagem com um exemplo para reduzir a condição. Se você pode me mostrar de onde você obtém as condições.
anthony willis muñoz
0

Penso que o JavaScript foi criado com um objetivo para que um desenvolvedor não note a diferença entre uma expressão e uma chamada de função em relação ao desempenho.

Em C ++, há uma palavra inline- chave para marcar uma função. Por exemplo:

inline bool userCheck()
{
    return isAuthorized;
}

Isso foi feito para eliminar uma chamada de função. Como resultado, o compilador substitui todas as chamadas userCheckpelo corpo da função. Razão para inovar inline? Um aumento de desempenho.

Portanto, acho que o tempo de execução de uma chamada de função com uma expressão, provavelmente, é mais lento que a execução da expressão. Mas também acho que você não notará uma diferença no desempenho se tiver apenas uma expressão na função.

alexander.sivak
fonte