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) {
}
}
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?
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!!
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
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.