Why angular2 executes methods several times?

2020-03-30 03:15发布

问题:

my application structure looks like this:

ts:

...
export class TodoListComponent {

    get sortedTodos():ITodo[] {
            console.log(this.counter++);
            ...
        } 
    ....

html:

  <div class="todo-item" *ngFor="let todo of sortedTodos" [class.completed]="todo.completed">
        <todo-list-item [todo]="todo" class="todo-item" (deleted)="onTodoDeleted(todo)"
                        (toggled)="onTodoUpdated($event)"></todo-list-item>
    </div>

If I start application I see in console:

1
2
3
4
5
6

I really confusing about this behaviour. for me it looks very strange and I think it can lead to bugs and performance issues. Please explain why it executes 6! times when I load page at once.

I am not sure that I provided all needed information in topic. Feel free to request womething else. Also all code base can be found bitbucket repo link

P.S.

full ts file content:

import {Component, Input, Output, EventEmitter} from "@angular/core"

import {ITodo} from "../../shared/todo.model";
import {TodoService} from "../../shared/todoService";

@Component({
    moduleId: module.id,
    selector: "todo-list",
    templateUrl: "todo-list.component.html",
    styleUrls: ["todo-list.component.css"],
})
export class TodoListComponent {
    @Input() todos:ITodo[];

    @Output() updated:EventEmitter<ITodo> = new EventEmitter<ITodo>();
    @Output() deleted:EventEmitter<ITodo> = new EventEmitter<ITodo>();

    get sortedTodos():ITodo[] {
        return !this.todos ? [] :
            this.todos.map((todo:ITodo)=>todo)
                .sort((a:ITodo, b:ITodo)=> {
                    if (a.title > b.title) {
                        return 1;
                    } else if (a.title < b.title) {
                        return -1;
                    }
                    return 0;
                })
                .sort((a:ITodo, b:ITodo)=> (+a.completed - (+b.completed)));
    }

    onTodoDeleted(todo:ITodo):void {
        this.deleted.emit(todo);
    }

    onTodoUpdated(todo:ITodo):void {
        this.updated.emit(todo);
    }

    constructor(private todoService:TodoService) {
    }
}

回答1:

It executes 6 times because:

There is a tick method in ApplicationRef class and it is executed 3 times by 2 change detection cycles. If you will enable production mode by calling enableProdMode() it will be executed 3 times

ApplicationRef is an reference to an Angular application running on a page. The tick method is running change detection from the root to leaves.

Here is how tick method looks (https://github.com/angular/angular/blob/2.3.0/modules/%40angular/core/src/application_ref.ts#L493-L509):

tick(): void {
  if (this._runningTick) {
    throw new Error('ApplicationRef.tick is called recursively');
  }

  const scope = ApplicationRef_._tickScope();
  try {
    this._runningTick = true;
    this._views.forEach((view) => view.ref.detectChanges()); // check
    if (this._enforceNoNewChanges) {
      this._views.forEach((view) => view.ref.checkNoChanges()); // check only for debug mode
    }
  } finally {
      this._runningTick = false;
      wtfLeave(scope);
  }
}

For debug mode tick starts two change detection cycles. So detectChangesInternal within compiled view will be called twice.

And as your sortedTodos property is a getter so it will be executed everytime as a function.

Read more about it here (Change Detection in Angular 2)

So then we know that our sortedTodos getter is called twice for one tick

Why is the tick method executed 3 times?

1) First tick is running manually by bootstrapping application.

private _loadComponent(componentRef: ComponentRef<any>): void {
  this.attachView(componentRef.hostView);
  this.tick();

https://github.com/angular/angular/blob/2.3.0/modules/%40angular/core/src/application_ref.ts#L479

2) Angular2 is running within zonejs so it's the main thing which manages change detection. Mentioned above ApplicationRef is subscribed to zone.onMicrotaskEmpty.

this._zone.onMicrotaskEmpty.subscribe(
  {next: () => { this._zone.run(() => { this.tick(); }); }});

https://github.com/angular/angular/blob/2.3.0/modules/%40angular/core/src/application_ref.ts#L433

onMicrotaskEmpty event is an indicator when zone gets stable

private checkStable() {
  if (this._nesting == 0 && !this._hasPendingMicrotasks && !this._isStable) {
    try {
      this._nesting++;
      this._onMicrotaskEmpty.emit(null); // notice this
    } finally {
      this._nesting--;
      if (!this._hasPendingMicrotasks) {
        try {
          this.runOutsideAngular(() => this._onStable.emit(null));
        } finally {
          this._isStable = true;
        }
      }
    }
  }
}

https://github.com/angular/angular/blob/2.3.0/modules/%40angular/core/src/zone/ng_zone.ts#L195-L211

So after some zonejs task this event is emitted

3) You're using angular2-in-memory-web-api package and when you're trying to get mock data it does following:

createConnection(req: Request): Connection {
    let res = this.handleRequest(req);

    let response = new Observable<Response>((responseObserver: Observer<Response>) => {
      if (isSuccess(res.status)) {
        responseObserver.next(res);
        responseObserver.complete();
      } else {
        responseObserver.error(res);
      }
      return () => { }; // unsubscribe function
    });

    response = response.delay(this.config.delay || 500); // notice this
    return {
      readyState: ReadyState.Done,
      request: req,
      response
    };
}

https://github.com/angular/in-memory-web-api/blob/0.0.20/src/in-memory-backend.service.ts#L136-L155

It start regular zonejs task cycle which makes zone unStable and finally after task execution is emitted described above onMicrotaskEmpty event again

You can find more details about zonejs here

  • http://blog.kwintenp.com/how-the-hell-do-zones-really-work/
  • http://blog.thoughtram.io/angular/2016/02/01/zones-in-angular-2.html

Recap:

So as you can see your solution a bit wrong. You shouldn't use getter or function as binding within your template. Possible solution you can find here

  • why *ngIf in angular 2 always is executing when use function?


回答2:

This is how Template expressions works in Angular 2, Angular executes template expressions more often than we think.They can be called after every keypress or mouse move.

There are Expression guidelines so that there is no bugs or performance issues.

Template expressions can make or break an application. Please follow these guidelines:

The only exceptions to these guidelines should be in specific circumstances that you thoroughly understand.

NO VISIBLE SIDE EFFECTS

A template expression should not change any application state other than the value of the target property.

This rule is essential to Angular's "unidirectional data flow" policy. We should never worry that reading a component value might change some other displayed value. The view should be stable throughout a single rendering pass.

QUICK EXECUTION

Angular executes template expressions more often than we think. They can be called after every keypress or mouse move. Expressions should finish quickly or the user experience may drag, especially on slower devices. Consider caching values computed from other values when the computation is expensive.

SIMPLICITY

Although it's possible to write quite complex template expressions, we really shouldn't.

A property name or method call should be the norm. An occasional Boolean negation (!) is OK. Otherwise, confine application and business logic to the component itself, where it will be easier to develop and test.

IDEMPOTENCE

An idempotent expression is ideal because it is free of side effects and improves Angular's change detection performance.

In Angular terms, an idempotent expression always returns exactly the same thing until one of its dependent values changes.

Dependent values should not change during a single turn of the event loop. If an idempotent expression returns a string or a number, it returns the same string or number when called twice in a row. If the expression returns an object (including an Array), it returns the same object reference when called twice in a row.

Number 6 in your case depends upon how many expression you have in your HTML template.

Hope this helps!!



回答3:

The NgFor directive instantiates a template once per item from an iterable. The context for each instantiated template inherits from the outer context with the given loop variable set to the current item from the iterable.

I would assume you have 6 items in your ITodo[] returned by getSortedTodos(), and for each loop in *ngFor="let todo of sortedTodos", get in TodoListComponent is called once, which is why you see six numbers been printed.

Source: https://angular.io/docs/ts/latest/api/common/index/NgFor-directive.html



回答4:

you are doing it wrong ..

*ngFor="let todo of sortedTodos"

this is not intelligent code. If one todo changes then it will re-render the entire list.

You need to use trackBy. This introduces intelligence. When using trackBy Angular2 only updates the list items that change.

This should reduce the number of times the code is executed.