Change detection is one of the most important features in Angular 2. It covers everything from how is data-binding working to who is checking changes? By understanding these important topics, you can see the logic that is running internally in Angular2. And, you can also understand ways of using performance tuning. By controlling change detection, you can build a dramatically faster application.
At first, you should review more about rendering. Rendering is a process to map models to views. The models can be primitives, objects, arrays or any JavaScript data. And the views can be headers, paragraphs, forms, buttons or any user-interface elements. Generally, those are represented as the Document Object Model (DOM).
Well, a simple rendering example is as follows:
<h1
id=
"greeting"
></h1>
<script>
document
.
getElementById
(
"greeting"
).
innerHTML
=
"Hello World!"
;
</script>
This is a very easy example, because this model will never change, so rendering is needed only once. And it gets so complex when the data changes at runtime. Rendering multiple times to sync models and views, we must think about the following things:
The basic role of change detection is to manage these things above. When a change happens, a manager of the component states, change detector, detects the change and notifies it to the renderer to update the view. Simply, a change detector has two tasks:
Well, we figured out about the roll of change detection. But then you may wonder; “what’s a change?”. A change is the difference between an old model and a new model. In other words, a change makes a new model. Let’s look at the following code:
@
Component
({
template
:
`
<
span
>
{{
counter
}}
<
/span>
<
button
(
click
)
=
"countUp()"
>
Count
up
<
/button>
`
})
class
MyComponent
{
counter
=
0
;
countUp() {
this
.
counter
++
;
}
}
Beginning rendering, {{counter}}
in the template will be rendererd as 0
by view interpolation. And when the button is clicked, the counter
property will be changed in the click event handler, and finally counted up so counter
will be applied to the view; <span>1</span>
.
In this case, click event causes a change and the change is a mutation of the property.
Let’s take a look at the next example:
class
MyComponent
{
counter
=
0
;
ngOnInit() {
setInterval
(()
=>
{
this
.
counter
++
;
},
1000
);
}
}
This component has a timer to count up its counter every second. In this case, it’s timer event causes a change. Finally, let’s look at the following example:
class
MyComponent
{
data
=
{};
constructor
(
private
http
:Http
)
{}
ngOnInit() {
this
.
http
.
get
(
'/data.json'
)
.
map
(
res
=>
res
.
json
())
.
subscribe
(
data
=>
{
this
.
data
=
data
;
});
}
}
This component will send an HTTP request once it is initialized.And when that response will be back, the data
property of the component will be updated.
In this case, it’s a XHR callback that causes a change.
In summary, there are three things that can cause a change of the model:
And these have a thing in common: they are asynchronous operations. And now, we can say also that: all asynchronous operations can cause changes.
Great, you learned about what causes changes and when it’s caused! But you don’t know yet who notifies the changes to views. So next, we’ll dive into a mechanism that allows Angular to detect changes anytime. It’s called Zone.
Zones is one of proposals of the next ECMAScript specification. An Angular team is developing its implementation as zone.js. It says in that source code:
Zone is a mechanism for intercepting and keeping track of asynchronous work.
A Zone is a global object that is configured with rules about how to intercept and keep track of the asynchronous callbacks. Zone has these responsibilities:
As a simple example, consider the following code:
Zone
.
current
.
fork
({
onInvokeTask
:
(
parent
,
current
,
target
,
task
)
=>
{
console
.
log
(
'Before task'
);
parent
.
invokeTask
(
target
,
task
);
console
.
log
(
'After task'
);
}
}).
run
(()
=>
{
setTimeout
(()
=>
{
console
.
log
(
'Task'
);
},
1000
);
});
The above will log:
'Before task' 'Task' 'After task'
This is not magic! Zones allow you to hook handlers on any async tasks.
In Angular, there is NgZone, which is a customized zone for Angular.
You can see that at the implementation of ApplicationRef
:
// summarized code
class
ApplicationRef
{
constructor
(
private
zone
:NgZone
)
{
this
.
zone
.
onMicrotaskEmpty
.
subscribe
(()
=>
{
this
.
zone
.
run
(()
=>
{
this
.
tick
();
});
});
}
tick() {
this
.
changeDetectorRefs
.
forEach
((
ref
)
=>
ref
.
detectChanges
());
}
}
In the above, when the zone has no tasks, a handler of that event calls tick()
. It executes change detection for each component of the application.
Summarizing the above:
It’s probably difficult to understand all about zones, but its behaviors in the application is simple. You don’t have to consider about when and where a change happens. Angular is observing those always and detects all changes.
So far, we outlined the change detection of Angular 2. Here, we’ll talk about its performance and how data-binding works in your application.
As you know, an application is a tree of components. And now an important thing is that each component has its own change detector. It means that an application is a tree of change detectors.
By the way, you might wonder. Who does make change detectors? It’s good question. They’re made by code generation. Angular 2 generates them for each component. And those codes are never in polymorphic. There are monomorphic VM-friendly codes. This is one of why new Angular is dramatically fast. Basically, each component can execute hundreds of thousands of detections within a couple of milliseconds. You can make fast your application without performance tuning.
The another reason is caused by the change detector tree. In Angular 2, any data flows from top to bottom. Let’s see the following components:
@
Component
({
selector
:
'child'
,
template
:
'<p>{{text}}</p>'
})
class
ChildComponent
{
@
Input
()
text
;
}
@
Component
({
selector
:
'parent'
,
template
:
'<child [text]="foo"></child>'
,
directives
:
[
ChildComponent
]
})
class
ParentComponent
{
foo
=
'bar'
;
}
Change detection always begins at the root component. So in the above example, a detection of ParentComponent
is earier than ChildComponent
, and as a result, the foo
property is passed to the child as text
input. Then, ChildComponent
detects a change of text
property; it changed from ""
into "bar"
.
This is simple but very important. After ParentComponent
was checked, its change detector doesn’t have to run, because it can be updated by only its parent. For single change detection, every component will be checked only once.
When any changes of input properties in your component are detected, an event is caused. Then you can get detail those changes as an argument. For example:
import
{
OnChanges
}
from
'@angular/core'
;
@
Component
({...})
class
MyComponent
implements
OnChanges
{
@
Input
()
prop
;
ngOnChanges
(
changes
:
{[
propName
:string
]
:
SimpleChange
})
{
let
newValue
=
changes
[
'prop'
].
currentValue
;
}
}
The OnChanges
interface defines a method, ngOnChanges
.
The argument, changes
, has all of the changes of the component as a key-value map. A value of the map is SimpleChange
, which has two properties: previousValue
and currentValue
. The earlier is an old value before the change, and the later is a new value.
A point you should be careful with is that the hook won’t be called by itself. An example is the following:
@
Component
({...})
class
MyComponent
{
@
Input
()
prop
=
null
;
ngOnChanges
(
changes
)
{
// never called after onSomeEvent
}
onSomeEvent() {
this
.
prop
=
someExpression
();
}
}
prop
is a property of MyComponent
and it is modified by itself.
In a case like this, ngOnChanges
is never called. It’s called only when its property is changed by its parent. So, you must never modify any input properties in your own component. You should access those as read-only.
Now, you’ve finished learning about the basis of change detection! And later, we’ll talk about how to tune your change detection. In Angular 2, you can configure change detection behavior for your application and optimize it. As mentioned earlier, change detection is very fast without any tuning. But if you want, you may make its change detection smarter and quite faster.
In order to customize change detection of your component, there is a utility: ChangeDetectionStrategy
. Using this, you can change the strategy of change detection for each component.
First, let’s imagine a component like this:
@
Component
({
selector
:
'profile-card'
,
template
:
`
<
div
>
<
profile
-
name
[
name
]
=
"profile.name"
><
/profile-name>
<
profile
-
age
[
age
]
=
"profile.age"
><
/profile-age>
<
/div>
`
,
directives
:
[
ProfileNameComponent
,
ProfileAgeComponent
]
})
class
ProfileCardComponent
{
@
Input
()
profile
;
}
ProfileCardComponent
is a component, which has profile
field as an input property. And its template (in other words, view) depends on only that property. So, this component and its child components depend on an input property.
Without a strategy, change detection runs from the root component to every leaf component. But in this case, it’s unnecessary to check children of ProfileCardComponent
if profile
is not changed. Simply, you can stop checking at ProfileCardComponent
. Well, there is a strategy for the case that uses OnPush
.
OnPush* is an option of change detection strategy. Let’s look at the following code:
@
Component
({
selector
:
'profile-card'
,
template
:
`
<
div
>
<
profile
-
name
[
name
]
=
"profile.name"
><
/profile-name>
<
profile
-
age
[
age
]
=
"profile.age"
><
/profile-age>
<
/div>
`
,
directives
:
[
ProfileNameComponent
,
ProfileAgeComponent
],
changeDetection
:ChangeDetectionStrategy.OnPush
,
})
class
ProfileCardComponent
{
@
Input
()
profile
;
}
There is a new component setting, changeDetection
, which take a strategy provided as a static field of ChangeDetectionStrategy
. Using this strategy, the component skips its children’s change detection when no changes are caused by its parent. Reducing the number of checks affects a performance of the application directly. As far as possible, you should use the OnPush
strategy on your components, and for this, you should increase components that depend on only input properties.
Using the OnPush strategy, it’s very important to learn about mutable and immutable. Basically, change detectors can detect only reference changes. Let’s see the next bad example:
@
Component
({
template
:
`
<
div
>
<
profile
-
card
[
profile
]
=
"profile"
><
/profile-card>
<
/div>
`
,
directives
:
[
ProfileCardComponent
],
})
class
AppComponent
{
profile
;
ngOnInit() {
this
.
profile
=
{
name
:
'Brad Green'
};
}
changeProfile() {
this
.
profile
.
name
=
'Igor Minar'
}
}
In this case, the profile
property of the AppComponent
is modified but its reference is never changed. With mutable data like this, the OnPush strategy doesn’t work well because the change detector of AppComponent
cannot know whether profile
was changed. So, the OnPush strategy needs immutable data to detect changes at the component closer to the root.
This is the rewritten code:
@Component({ template: ` <div> <profile-card [profile]="profile"></profile-card> </div> `, directives: [ProfileCardComponent], }) class AppComponent { profile; ngOnInit() { this.profile = new Profile({ name: 'Brad Green' }); } changeProfile() { let newProfile = profile.setName('Igor Minar'); // make new reference profile === newProfile; // false this.profile = newProfile; } }
The reference of profile
is changed in changeProfile()
. Because profile
became immutable, ProfileCardComponent
can check whether profile
was changed strictly, and it can control its children’s change detection.
The OnPush strategy is an easy and great way to improve the performance of your application. Well, there is another way to get better performace. It is to use observable properties with ChangeDetectorRef
utility. This way allows you to control all of the change detection manually.
ChangeDetectorRef
is a reference of the compoment’s change detector. This can be implemented by dependency injection at the component:
import
{
ChangeDetectorRef
}
from
'@angular/core'
;
@
Component
({})
class
MyComponent
{
constructor
(
private
cdRef
:ChangeDetectorRef
)
{}
}
ChangeDetectorRef
has some methods. In those, markForCheck()
is the most useful method. It marks components from the root to the place as to be checked. Let’s take a look at the following:
@
Component
({
selector
:
'child'
,
template
:
`
<
p
>
{{
counter
}}
<
/p>`,
changeDetection
:ChangeDetectionStrategy.OnPush
,
})
class
ChildComponent
{
counter
=
0
;
constructor
(
private
cdRef
:ChangeDetectorRef
)
{}
ngOnInit() {
setInterval
(()
=>
{
this
.
counter
++
;
this
.
cdRef
.
markForCheck
();
},
1000
);
}
}
@
Component
({
selector
:
'parent'
,
template
:
`
<
child
><
/child>`,
changeDetection
:ChangeDetectionStrategy.OnPush
,
})
class
ParentComponent
{
}
ChildComponent
is defined as the OnPush component, but it doesn’t have any input properties. When will ChildComponent
be checked? Well, look at its ngOnInit
!
In the interval function, cdRef.markForCheck()
is called per 1 second. Even without any changes or events, if markForCheck()
was called, that component is included in the next change detection process.
Next, we talk about cdRef.detach()
and cdRef.reattach()
.
They allow you to turn on/off change detection at the component.
Let’s see the following example:
@
Component
({
template
:
`
Detach
:
<
input
type
=
"checkbox"
(
change
)
=
"detachCD($event.target.checked)"
>
<
p
>
{{
counter
}}
<
/p>
`
,
})
class
ChildComponent
{
counter
=
0
;
constructor
(
private
cdRef
:ChangeDetectorRef
)
{}
ngOnInit() {
setInterval
(()
=>
{
this
.
counter
++
;
},
1000
);
}
detachCD
(
checked
)
{
if
(
checked
)
{
this
.
cdRef
.
detach
();
}
else
{
this
.
cdRef
.
reattach
();
}
}
}
This component has a checkbox to toggle its own change detection.
When the checkbox is checked, cdRef.detach()
will be called,
and after that, the component and its children will be never checked.
As a result, the view of the sub-tree from the component will be frozen.
And when you uncheck it, cdRef.reattach()
will be called. After that, the component will join the change detector tree again.
Change detection of Angular 2 is a great system to manage your application states, and it can provide you with an easy way to improve the performance.