Skip to content

Instantly share code, notes, and snippets.

@chidumennamdi
Created August 22, 2018 16:52
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save chidumennamdi/1ff2d570742d8b6c3c4895934ba6503b to your computer and use it in GitHub Desktop.
Save chidumennamdi/1ff2d570742d8b6c3c4895934ba6503b to your computer and use it in GitHub Desktop.

What you always wanted to know about Change Detection, OnPush Change Detection Strategy and Default Change Detection Strategy

View and Tree of Views

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 Cycle

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() decoratedproperties. 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 and Change Detection States

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.

Default CD Strategy

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

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.

OnPush Triggers

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:

  1. from ApplicationRef class via the tick method.
  2. 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.

ChangeDetectorRef class

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.

Conclusion

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 !!!

Helpful links

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment