Kątowe - * ngIf vs proste wywołania funkcji w szablonie


14

Przepraszam, jeśli tutaj już udzielono odpowiedzi, ale nie mogłem znaleźć żadnego dopasowania dla naszego konkretnego scenariusza, więc proszę!

W naszym zespole programistów rozmawialiśmy o wywołaniach funkcji w szablonach kątowych. Zgodnie z ogólną zasadą zgadzamy się, że nie powinieneś tego robić. Próbowaliśmy jednak omówić, kiedy może być w porządku. Pozwól, że dam ci scenariusz.

Załóżmy, że mamy blok szablonu zawarty w ngIf, który sprawdza wiele parametrów, jak tutaj:

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

Czy byłaby znacząca różnica w wydajności w porównaniu do czegoś takiego:

Szablon:

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

Maszynopis:

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

Podsumowując pytanie, czy ostatnia opcja miałaby znaczny koszt wydajności?

Wolelibyśmy zastosować drugie podejście w sytuacjach, w których musimy sprawdzić więcej niż 2 warunki, ale wiele artykułów online mówi, że wywołania funkcji ZAWSZE są złe w szablonach, ale czy to naprawdę problem w tym przypadku?


7
Nie zrobiłby tego. Jest też bardziej przejrzysty, ponieważ sprawia, że ​​szablon jest bardziej czytelny, warunek jest łatwiejszy do przetestowania i wielokrotnego użytku, a ponadto masz do dyspozycji więcej narzędzi (cały język TypeScript), aby uczynić go tak czytelnym i wydajnym, jak to możliwe. Wybrałbym jednak znacznie wyraźniejszą nazwę niż „userCheck”.
JB Nizet

Bardzo dziękuję za wkład :)
Jesper

Odpowiedzi:


8

Starałem się również unikać wywołań funkcji w szablonach w jak największym stopniu, ale twoje pytanie zainspirowało mnie do szybkiego zbadania:

Dodałem inny przypadek z userCheck()wynikami buforowania

*ngIf="isUserChecked"

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

Przygotowałem demo tutaj: https://stackblitz.com/edit/angular-9qgsm9

Zaskakująco wygląda na to, że nie ma między nimi żadnej różnicy

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

I

*ngIf="userCheck()"

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

I

*ngIf="isUserChecked"

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

Wygląda na to, że dotyczy to prostego sprawdzania właściwości, ale na pewno będzie różnica, jeśli chodzi o jakiekolwiek asyncakcje, na przykład pobierające, które oczekują na interfejs API.


10

To dość uparta odpowiedź.

Korzystanie z takich funkcji jest całkowicie dopuszczalne. Sprawi, że szablony będą znacznie wyraźniejsze i nie spowoduje znacznego obciążenia. Jak powiedział wcześniej JB, stworzy również znacznie lepszą bazę do testów jednostkowych.

Myślę również, że każde wyrażenie, które masz w szablonie, zostanie ocenione jako funkcja przez mechanizm wykrywania zmian, więc nie ma znaczenia, czy masz je w szablonie, czy w logice komponentów.

Po prostu ogranicz logikę wewnątrz funkcji do minimum. Jeśli jednak jesteś ostrożny, jeśli chodzi o wpływ na wydajność, jaki może mieć taka funkcja, zdecydowanie zalecamy skorzystanie ChangeDetectionStrategyz niej OnPush, co i tak jest uważane za najlepszą praktykę. Dzięki temu funkcja nie będzie wywoływana w każdym cyklu, tylko gdy Inputzmiany, jakieś zdarzenie wydarzy się w szablonie itp.

(za pomocą itp., ponieważ nie znam już drugiego powodu) .


Osobiście uważam, że jeszcze przyjemniej jest użyć wzorca Observables, wtedy możesz użyć asyncpotoku i tylko wtedy, gdy zostanie wysłana nowa wartość, szablon zostanie ponownie oceniony:

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

Następnie możesz po prostu użyć w szablonie w ten sposób:

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

Jeszcze inną opcją byłoby użycie ngOnChanges, jeśli wszystkie zmienne zależne od komponentu są danymi wejściowymi i masz dużo logiki, aby obliczyć pewną zmienną szablonu (co nie jest pokazane w przypadku):

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;
  }
}

Które możesz użyć w swoim szablonie w ten sposób:

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

Dziękuję za odpowiedź, bardzo wnikliwa. Jednak w naszym konkretnym przypadku zmiana strategii wykrywania nie jest opcją, ponieważ dany komponent wykonuje żądanie get, a zatem zmiana nie jest związana z konkretnym wejściem, ale raczej żądaniem get. Jest to jednak bardzo przydatna informacja do opracowania przyszłych komponentów, w których zmiana zależy od zmiennych wejściowych
Jesper

1
@Jesper, jeśli komponent wykonuje żądanie get, masz już Observablestrumień, dzięki czemu będzie idealnym kandydatem do drugiej opcji, którą pokazałem. Tak czy inaczej, cieszę się, że mogę dać ci wgląd
Poul Kruijt

6

Z wielu powodów nie jest zalecany główny:

Aby ustalić, czy konieczne jest ponowne renderowanie userCheck (), Angular musi wykonać wyrażenie userCheck (), aby sprawdzić, czy zmieniła się jego wartość zwracana.

Ponieważ Angular nie jest w stanie przewidzieć, czy zmieniła się wartość zwracana przez userCheck (), musi wykonywać tę funkcję za każdym razem, gdy wykrywane są zmiany.

Więc jeśli wykrywanie zmian działa 300 razy, funkcja jest wywoływana 300 razy, nawet jeśli jej wartość zwrotna nigdy się nie zmienia.

Rozszerzone wyjaśnienia i więcej problemów https://medium.com/showpad-engineering/why-you-should-never-use-function-calls-in-angular-template-expressions-e1a50f9c0496

Problem pojawiający się, gdy twój komponent jest duży i bierzesz udział w wielu wydarzeniach związanych ze zmianą, jeśli komponent będzie niewielki i po prostu weźmiesz udział w kilku wydarzeniach, nie powinien stanowić problemu.

Przykład z obserwowalnymi

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;
   }
})
);

Następnie możesz użyć go jako obserwowalnego z potokiem asynchronicznym w swoim kodzie.


2
Cześć, chciałem tylko zauważyć, że propozycja użycia zmiennej jest bardzo myląca. Zmienna nie aktualizuje się, gdy zmienia się
jedna

1
I niezależnie od tego, czy wyrażenie znajduje się bezpośrednio w szablonie, czy jest zwracane przez funkcję, należy je oceniać przy każdym wykryciu zmiany.
JB Nizet

Tak, to prawda, że ​​naprawdę przepraszam, że nie zrobię złych praktyk
Anthony willis muñoz

@ anthonywillismuñoz Więc jak podejdziesz do takiej sytuacji? Po prostu żyj w wielu trudnych do odczytania warunkach w * ngIf?
Jesper

1
to zależy od twojej sytuacji, masz kilka opcji w średnim poście. Ale myślę, że używasz obserwowalnych. Zedytuję post z przykładem, aby zmniejszyć warunek. czy możesz mi pokazać, skąd bierzesz warunki.
Anthony willis muñoz

0

Myślę, że JavaScript został stworzony w celu, aby programista nie zauważył różnicy między wyrażeniem a wywołaniem funkcji w odniesieniu do wydajności.

W C ++ istnieje słowo kluczowe inlinedo oznaczenia funkcji. Na przykład:

inline bool userCheck()
{
    return isAuthorized;
}

Dokonano tego w celu wyeliminowania wywołania funkcji. W rezultacie kompilator zastępuje wszystkie wywołania userChecktreścią funkcji. Powód innowacji inline? Wzrost wydajności.

Dlatego myślę, że czas wykonania wywołania funkcji z jednym wyrażeniem jest prawdopodobnie dłuższy niż wykonanie tylko wyrażenia. Myślę jednak, że nie zauważysz różnicy w wydajności, jeśli masz tylko jedno wyrażenie w funkcji.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.