What you always wanted to know about Change Detection, OnPush Change Detection Strategy and Default Change Detection Strategy
An Angular application is a tree of Components. Components are the views Angular renders, so it can be put in another way, Angular is a tree of views.
A View is a fundamental building block of the application UI. It is the smallest grouping of Elements which are created and destroyed together.
An Angular Component is a single View. A component is composed of three parts:
- Markup (View)
- Metadata (
Decorator
) - A class (Controller)
All this combined together lets us create a new HTML language.
Components are a feature of Angular that let us create a new HTML language and they are how we structure Angular applications.
Tree of Views
As we stated earlier, an Angular app is nothing but a tree of components. The bootstrapping/rendering of the app starts from the root of the tree up to the "branches".
Components are structured in a parent/child relationship, when each component renders, its children Components are rendered recursively.
For example, we have four components: AppComponent, BookListComponent, BookComponent, FoodListComponent, FoodComponent.
https://gist.github.com/c6fa06358a7bb8e939862a3aa9c66b1f
AppComponent is the root component with children Components BookListComponent
and FoodListComponent
. BookListComponent is parent to BookComponent and FoodListComponent is parent to FoodComponent.
The tree of our app can be represented like this:
https://gist.github.com/8bc985b4b0e3c8ecbd0820ff0329cdef
When [app] is rendered, it renders [food-list] then, [food]. Next, [book-list] is rendered and followed by [book]. At the end we see something like this:
https://gist.github.com/00c3ea3034402bcc761eab11d58c2bf7
As we already know a Component == a View. Each View has a state, that decides whether to update its UI and its children or simply ignore it.
Change Detection refreshes the DOM tree to display changed states in an app.
The goal of change detection is always projecting data and its change.
Angular uses Zone.js to detect when to trigger a CD cycle. Angular actually wraps Zone.js functionality into a class NgZone. Zone.js emits an async event whenever an async operation is executed. Async operations are emitted by:
- Events
- XHRs
- Timers
Angular runs a CD cycle whenever it detects any of these events through Zone.js. Angular CD cycle is triggered by a method tick in the ApplicationRef class.
https://gist.github.com/b06b423fcfa0e64e6630b7e08179cb72
It loops through the _views array and calls their detectChanges method. We see the _views are of type InternalViewRef. InternalViewRef is a subclass of ViewRef:
https://gist.github.com/e05c05f60c715a108d1706a1932bd83e
The ViewRef class embodies a component view in Angular. A Component declared in Angular is represented by a ViewRef class, manipulations in the Component's view is done through its ViewRef class instance.
https://gist.github.com/62cd71f0cba9a7eb03470904162c671d
ViewRef extends ChangeDetectorRef class:
https://gist.github.com/c75ccf9b86e05548129a0d8bf4311133
So we see internalViewRef has all properties and methods of ViewRef and ChangeDetectorRef classes.
When we want to manipulate CD from a Component, we inject the ChangeDetectorRef class.
https://gist.github.com/6425804cfc279fe9d1a221fabbc24b3f
The ChangeDetectorRef is an abstract class as we saw above, so on injection:
https://gist.github.com/fb73b971dc34bd34a51986faa39bfd2a
The createChangeDetector function returns an implementation of ChangeDetectorRef.
https://gist.github.com/276698a6751dfccb76a92e877b887a3d
ViewRef_
class implements the interface InternalViewRef and defines the methods InternalViewRef
inherited from its parent classes ChangeDetector and ViewRef:
https://gist.github.com/7fb3562de1bc7d20bbda0d4b630fdc67
So ViewRef_ can be of types ChangeDetector, ViewRef or InternalViewRef. The createChangeDetectorRef function returns an instance of ChangeDetectorRef class, it will contain only the methods in the abstract ChangeDetectorRef class, but now defined. Methods destroy, attachToViewContainerRef, attachToAppRef, detachFromAppRef, onDestroy, destroy, destroyed won't be available.
The detectChanges() method is what is called in the tick() method. The detectChanges method in turn calls a fucntion, checkAndUpdateView
:
https://gist.github.com/f817dda57eb1a7a6439893666111b4a3
The checkAndUpdateView is responsible for the change detection cycle. It recursively walks through the Angular's tree of views, checking and updating each component's view.
https://gist.github.com/ce697a60d2433a758e00f43acbb397e4
1. This checks the BeforeFirstcheck and FirstCheck state of the current view. If its the very first check it zeroes out the BeforeFirstCheck state and enables the FirstCheck state. Then, on the next CD run, it becomes the view's first check so the previously enabled FirstCheck flag is zeroed out. The initial state of any view is CatInit
.
https://gist.github.com/412a6c0f15292f06976135a4d1edb690
The CatInit has the BeforeFirstCheck, CatDetectChanges(enables Attached and ChecksEnabled flags) and InitState_BeforeInit flags set.
Read more about bitmasks here
2, shiftInitState function is called before each cycle of a view's check to detect whether this is in the initState for which we need to call ngOnInit, ngAfterContentInit or ngAfterViewInit lifecycle methods.
Lifecycle hooks are methods provided by Angular to let users run code in every step of a Component/Directive lifecycle.
The lifecycle hooks are:
- OnInit
- OnDoCheck
- OnChanges
- OnDestroy
- ngAfterContentInit
- ngAfterViewInit
- ngAfterContentChecked
- ngAfterViewChecked
Here, we are checking whether to set InitState_CallingOnInit
flag on the view state if the view currently has InitState_BeforeInit
state set.
3 This marks projected views for check. Projected views are elements in the ng-template tag marked as ProjectedView that it is attached to a container.
This function loops through the projected views and set the ViewState.CheckProjectedView
flag on each view.
4. checks and updates the Directives changed input @Input() decorated
properties. The first param is the current view object and the 2nd param is the type of action to be taken, ViewAction.CheckAndUpdate. ViewAction.CheckAndUpdate tells Angular we want to check the node for any changes if any update the Directive class properties with the changed values.
5. This function call runs CD for Embedded views in the view. Embedded views are generated whenever we use the ng-template
tag.
6. Runs updates on @ContentChild and @ContentChild queries in the view.
7. The shiftInitState is checking whether to set InitState_CallingAfterContentInit
flag if the InitState_CallingOnInit
flag is set in the current view state.
8. Here, ngAfterContentInit lifecycle hook is called on the view.
9. This update the bindings on elements in the current view if any have changed.
10. This runs CD on the view's children recursively, ie this checkAndUpdateView is called again with the child view as the argument.
11. ViewQuery nodes in the current view are checked for changes.
12. InitState_CallingAfterViewInit
flag is set on the view if it already has InitState_CallingAfterContentInit
flag enabled.
13. ngAfterViewInit and ngAfterViewChecked lifecycle hooks on the view are called here.
14. If the current view has OnPush CD strategy flag set on it. The ChecksEnabled flag set on its state is disabled.
15. Here, flags CheckProjectedViews
and CheckProjectedView
are disabled on the view.
16. InitState_AfterInit
flag is set if the view has InitState_CallingAfterViewInit
flag already present.
View States Each view in an Angular tree of views has possible states it can be in. These states decide whether to run a UI update/CD on a view and its children.
The possible states a view can be in:
https://gist.github.com/9fc2694a3f58cbee85e8d3f2371d0501
We will see how this states affect CD run on a view in the later in this article.
Change Detector Status This defines the possible state of the change detector.
https://gist.github.com/b1e6e8c86afee15156dced24dcf8acec
CheckOne This employed by the OnPush CD strategy. It checks the component once
and sets its state to Checked
.
Checked This is the state to which OnPush components are set after the initial CD run on them.
CheckAlways This is employed by Default CD strategy. CD is always
run on the components.
Detached The state to which components are set when they are detached from the CD tree. In this state, CD run on the view and its children are skipped.
Errored This state indicates that CD encountered an error on the view. This is caused when a directive's lifecycle method call throws or a binding in the view has issues. In this state, a CD is skipped in this view.
Destroyed This state indicates the component has been destroyed.
As the name implies, it is the default CD strategy every view has. It is set whenever we create a Component via @Component decorator:
https://gist.github.com/ed4cee9b677c53cbd635a5c4d2194750
This the configuration metadata for an Angular component. We see that it has changeDetection property. This is what is set to tell Angular that our view has either OnPush or Default CD strategy. It sets the CD strategy to use when propagating the component's bindings.
https://gist.github.com/e073703d8b7130cb7ba9db0d1cb0896a
Here, we see the Component decorator changeDetection
property is set to ChangeDetectionStrategy.Default
even before we use it. So, whenever we docorate our class with @component the changeDetection strategy is set to Default.
But, if we want to change it, we can override it by simply doing this:
https://gist.github.com/44db301aaf054b4912098bd2005bf3a4
This changes the CD strategy to OnPush removing the initial Default setting.
With Default CD strategy, our Component is checked always in every CD run until it is deactivated.
https://gist.github.com/6678fd7ebf4fd958b1f766585cad8520
Now, imagine our app grows to be complex with hundreds (or more) of bindings (template bindings, property bindings, query bindings) to update on every single CD run. This will hugely impact the performance of our app.
But, what if we could tell Angular when to run CD? Wouldn't that be ideal? I guess it would.
OnPush CD strategy tells Angular to run CD on the Component for the first time and deactivate. Deactivate? Yes, on subsequent CD runs the Component will be skipped.
OnPush CD strategy is set on a Component like this:
https://gist.github.com/d52b34791cc28587a6cdb36967374258
The component factory will look like this:
https://gist.github.com/82ee39d1ea042b402995dfcab7bb47cc
The first arg in the viewDef is the ViewState. During CD cycles this is the param Angular checks to see if would skip or run CD on a view.
There are cases whereby views with OnPush CD strategy can be run explicitly even after the initial CD run and deactivation. We will look at the cases below:
DOM Events
DOM Events cause CD to run on a view with OnPush Strategy.
For example:
https://gist.github.com/ccf891d25ced99828ca5cc7e6f4c39ad
When the Click button is clicked the count increments and the update will be reflected on the DOM. DOM events are async ops and setTimeout, XHRs etc are also asynchronous. But only DOM events runs UI updates on an OnPush component, other APIs won't work.
https://gist.github.com/f92d1f3d1214fac4a464d85358d76f48
The setTimeout function increments the count variable, but it won't be updated on the DOM. When we click the button the count value( plus the increments made by the setTimeout) will be reflected on the DOM.
Why, is this so? Why should it be only DOM events? To know the reason let's peek under the hood.
The factory generated by the DOMComponent will look like this:
https://gist.github.com/5b66306afdb8d3ef6c95b479ac3f3538
The outputs array holds the events to be registered against the button element. The handleEvent function is the global callback function for any event triggered by the button element. During view creation, Angular loops through the outputs array and attaches each event in the outputs array to the element.
https://gist.github.com/00ddf6a22679b192c65e491df648fa31
The listen call registers the event output.eventName
to the element listenTarget || el
with handleEventClosure as the callback function. The handleEventClosure function is a closure which from the renderEventHandlerClosure function. As handleEventClosure
== (event: any) => dispatchEvent(view, index, eventName, event)
. The dispatchEvent is called on any event emission (in our case a click event). The dispatchEvent function
https://gist.github.com/86d6df138ffa1c3815e79b6ac7aa2fa6
will call markParentViewsForCheck(startView);
and eventually call our handleEvent function.
The markParentViewsForCheck
function
https://gist.github.com/643400c5de50df0d6673415e748a1d86
enables the ChecksEnabled flag on components with OnPush CD Strategy an iterates upwards to its parent views enabling any view with the OnPush CD Strategy.
So when the callback function has exited, CD is run from the root view via tick(). Our component being set to ChecksEnabled will have its UI updated. That's CD will be run on our component because it will pass this check:
https://gist.github.com/41068f212c57d072e9bb2828fb54988f
Then after that it will be disabled:
https://gist.github.com/497d6fe17eeb29fa101ad91223c7dcd8
Remember, OnPush runs once and deactivates. We have seen why DOM events update UI on OnPush components but not by other async APIs.
detectChanges() call
The detectChanges() method is a method in the ChangeDetectorDef abstract class defined in the ViewRef_ class a subclass of ChangeDetectorRef.
This method runs CD on a view and its children.
For example:
https://gist.github.com/acb25d179340849ee1c9f6395d591ed7
We injected the ChangeDetector class so we could access and run its detectChanges method.
So, how would it run CD on this component whereas, it has already run once and been deactivated?
We can trigger CD from two places in Angular:
- from ApplicationRef class via the tick method.
- from ChangeDetector class via the detectChanges method.
The tick
method runs CD from the root view, whereas detectChanges
runs from the passed in view downwards to its children.
The checkAndUpdateView is the function that runs the CD and recursively calls its children. Looking back at the checkAndUpdateView function implementation in the Change Detection Cycle
section. We will see that UI updates 9.
and directives 4.
are performed first before the ChecksEnabled flag is disabled on OnPush components 14.
.
So if we could jump the gun and somehow manage to run from 4.
through to 9.
, we will have UI updated on an OnPush component even though deactivated. We will have to find a way to call the checkAndUpdateView
function with our OnPush view as the parameter.
Looking at the detectChanges method, we will see that
https://gist.github.com/7a76f26a3ce0e0d7b7894e3697b0cc88
it calls the checkUpdateANdView function with a view arg passed to it. So if we pass our OnPush view to it, the view's UI will be updated and the changes made to it will be rendered, yet it has long since been deactivated.
That's the reason we injected ChangeDetectorRef in our component, so we could explicitly run CD on our OnPush view via the checkAndUpdateView function.
Immutability check
How does this run CD on a deactivated OnPush component? we will see in a while.
Let's say our component has an input binding to receive data from its parent:
https://gist.github.com/3362407fcd40fa77d93a9ef436b93f75
and on the parent component, we have this:
https://gist.github.com/60c96f989d4b678b702ef6f25418f7b1
The IComponent has an input iHuman to receive an object from its parent PComponent.
Now, if we click on either Incr. Id
or Change Name
button, the view on IComponent won't be updated to reflect the new change on its property iHuman.
Why? There was no reference change, the object was mutated directly.
Although, IComponent shouldn't have been updated 'cos it is an OnPush component and has already been run once and deactivated. So why was it run? Simple, it was because of the input binding <my-i [iHuman]="pHuman"></my-i>
on its parent's template.
See the tree of the app:
https://gist.github.com/dc9e9717073209e5002d47426d713d56
CD runs from the top app-root
down to my-i
.
Next, let's look at the factory generated for the PComponent:
https://gist.github.com/2de82e6be63986167d76da84c2a422b1
During CD pass in the app-parent
PComponent, the updateDirectives arg is called (check 4.
in checkAndUpdateView function) via Services.updateDirectives. This function updates all directives bindings in the current view.
We see that the updateDirectives arg takes in _ck and _v, _ck refers to checkAndUpdateNode function and _v is the view definition of the view. It retrieves the current value of the bound variable pHuman and calls _ck with it along with other parameters, _v, 1 the index of the directive node to update, 0 the type of update to perform, currVal_0
the value to update the directive node with.
In the process of updating the directives properties,
https://gist.github.com/584bdf064993c430bc24b6c7937a53f9
The value to be updated is checked against the value in the directive's class in the checkBinding fucntion.
https://gist.github.com/0fb4e24f89dcf5eb29bf46befd626534
looseIdentical
uses the equality check ===
to check for reference change. If any it returns true, if not, false is returned.
So if our object's reference was changed, the changes
variable in the checkAndUpdateDirectiveInline function will be set to true, then, the directive's property will be updated via the updateProp
function.
https://gist.github.com/1cde9dd60b66433c40e8a15e8cb99bed
Here, the component view is retrieved and if has OnPush CD strategy, the ChecksEnabled flag is set on its state.
We have seen that reference change is checked on directives bindings to see if the directive is to be updated and if the view is OnPush component, it will be activated for the next CD run.
So, to make IComponent CD run. We have to change the reference of the pHuman object in PComponent before sending it to IComponent.
What does it mean, when we say to change the reference of a JS object? This simply means to change the memory address of pointed to by a variable. Let's explain further with the example:
https://gist.github.com/2c78f6173791511a96fa2789af1ea9c9
This is quite clear and explains the concept.
We now see for OnPush component to run on the next CD cycle, we have return new object each time the input bindings are to be updated, so we could have the ChecksEnabled
flags set.
We edit the PComponent:
https://gist.github.com/4b6e0a91357a038262e7a3a0fe1bb002
We create a new object and assign it to the pHuman
object. Now, there is a reference change. CD will be run on the IComponent when either of the buttons is clicked.
We saw what the ChangeDetectorRef is and what it does.
https://gist.github.com/4cb1c13ee008f53aa344a3d3c53f40a2
Now, let's explore its methods:
markForCheck
This method is used to enable ChecksEnabled
for OnPush parent components of an OnPush component.
https://gist.github.com/a17e56f9da391f11867dcb19f6069a79
Let's see where this method comes in handy.
https://gist.github.com/f6a1028b1e993dd94ea2276ee2fca891
This count won't be re-rendered afte 10 secs, despite setTimeout being an async op. To make the count to be reflected on the DOM on each setTimeout tick. We have to call the markForCheck() method, so on the next CD run the count will be rendered.
https://gist.github.com/efcfcba7a2c23774761b9ab37cbd11ce
We added the this.cd.markForCheck()
inside the settimeout() call after the this.count++
increment because on each CD run, ChecksEnabled is deactivated on OnPush components. So, on each setTimeout tick, the flag is enabled so CD will run on our component.
Now, it will work as expected.
detectChanges
This is the most useful and used method in the ChangeDetectorRef class. It runs CD on the current view (and on its children).
This method runs change detection for the current component view regardless of its state.
I concur with the above statement because it jumped the gun. The CD was run directly on the component.
Note: child views with OnPush setting or on a detached state will be skipped.
detach
This method disables CD run on the current view. A detached view is not checked until attached.
https://gist.github.com/9c1c3d0c969a112da56adbd712abd479
The component is detached from the CD tree on initialization, the count property keeps increasing but not reflected on the DOM. Whenever we want to see the current value of count, we click the ShowCount
button. It calls the detectChanges which re-renders the count property.
reattach
This is the opposite of detach. It undoes what detach did. This method enables the current view for check.
Re-attaches the previously detached view to the change detection tree.
https://gist.github.com/2d466b90b0deea0e75487538a67f7f7c
The view was detached on initialization. Then, we reattached the view to the CD tree when the count property is 100
. Then, the count property is displayed in the DOM from 100
onwards.
checkNoChanges
This method checks if no changes are made in the view's bindings. If any change is detected, expressionChangedAfterItHasBeenCheckedError
error is thrown.
Checks the change detector and its children, and throws if any changes are detected.
Pheew!!! This is one huge piece. If you made it made here, congratulations and thanks for the persistence.
We covered so many concepts in Angular: CD, CD Strategies (OnPush and Default), Tree of Views, View states, the ChangeDetectorRef class and its methods going through them with precision, explaining with examples what they are, how they work and how to use them.
If you have any question regarding any concept or code in this article, feel free to comment, email or DM me
Next thing, to follow and understand the call of functions in Angular, the browser debugger is a very good tool. It helped me understand what happens in runtime after going through the Angular sources. The step-over, step-in, step-in buttons are very useful for navigation and the Call Stack tool comes handy when you want to track the flow of functions already executed.
Let the debugger be your guide.
Thanks !!!