Skip to content

Instantly share code, notes, and snippets.

@chidumennamdi
Created August 22, 2018 16:47
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save chidumennamdi/e7e3f77c87a9bb7b751245b67ead1112 to your computer and use it in GitHub Desktop.
Save chidumennamdi/e7e3f77c87a9bb7b751245b67ead1112 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.

@Component({
  selector: 'food-list',
  template: `
    <food></food>
  `
})
export class FoodListComponent {}

@Component({
  selector: 'food',
  template: `
    I am a Food
  `
})
export class FoodComponent{}

@Component({
  selector: 'book-list',
  template: `
    <book></book>
  `
})
export class BookListComponent{}

@Component({
  selector: 'book',
  template: `
    I am a Book
  `
})
export class BookComponent {}

@Component({
  selector: 'app',
  template: `
    <book-list></book-list>
    <food-list></food-list>
  `

})
export class AppComponent {}

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:

      [app]
        |
    ------------------
   |                 |
[food-list]        [book-list]

  |                     |
[food]                [book]

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:

<app>
  <food-list>
    <food>
      I am a Food
    </food>
  </food-list>
  <book-list>
    <book>
      I am a Book
    </book>
  </book-list>
</app>

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.

class ApplicationRef {
  private _views: InternalViewRef[] = [];
  tick(): void {
      ...
      this._views.forEach((view) => view.detectChanges());
      ...
    }
}

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:

export interface InternalViewRef extends ViewRef {
  detachFromAppRef(): void;
  attachToAppRef(appRef: ApplicationRef): void;
}

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.

export abstract class ViewRef extends ChangeDetectorRef {
  abstract destroy(): void;

  abstract get destroyed(): boolean;

  abstract onDestroy(callback: Function): any 
}

ViewRef extends ChangeDetectorRef class:

export abstract class ChangeDetectorRef {
  abstract markForCheck(): void;

  abstract detach(): void;

  abstract detectChanges(): void;

  abstract checkNoChanges(): void;

  abstract reattach(): void;
}

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.

export class AppComponent {
    constructor(cd: ChangeDetectorRef) { ... }
}

The ChangeDetectorRef is an abstract class as we saw above, so on injection:

export function resolveDep(
    view: ViewData, elDef: NodeDef, allowPrivateServices: boolean, depDef: DepDef,
    notFoundValue: any = Injector.THROW_IF_NOT_FOUND): any {
...
        case ChangeDetectorRefTokenKey: {
          let cdView = findCompView(searchView, elDef, allowPrivateServices);
          return createChangeDetectorRef(cdView);
        }
...
}

The createChangeDetector function returns an implementation of ChangeDetectorRef.

export function createChangeDetectorRef(view: ViewData): ChangeDetectorRef {
  return new ViewRef_(view);
}

ViewRef_ class implements the interface InternalViewRef and defines the methods InternalViewRef inherited from its parent classes ChangeDetector and ViewRef:

export class ViewRef_ implements EmbeddedViewRef<any>, InternalViewRef {
  /** @internal */
  _view: ViewData;
  private _viewContainerRef: ViewContainerRef|null;
  private _appRef: ApplicationRef|null;

  constructor(_view: ViewData) { ... }

  get rootNodes(): any[] { ... }

  get context() { ... }

  get destroyed(): boolean { ... }

  markForCheck(): void { ... }
  detach(): void { ... }
  detectChanges(): void { ... }
  checkNoChanges(): void { ... }

  reattach(): void { ... }
  onDestroy(callback: Function) { ... }

  destroy() { ... }

  detachFromAppRef() { ... }

  attachToAppRef(appRef: ApplicationRef) { ... }

  attachToViewContainerRef(vcRef: ViewContainerRef) { ... }

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:

detectChanges(): void {
...
    Services.checkAndUpdateView(this._view);
...
}

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.

      export function checkAndUpdateView(view: ViewData) {
1.    if (view.state & ViewState.BeforeFirstCheck) {
          view.state &= ~ViewState.BeforeFirstCheck;
          view.state |= ViewState.FirstCheck;
        } else {
          view.state &= ~ViewState.FirstCheck;
1.    }
2.    shiftInitState(view, ViewState.InitState_BeforeInit, ViewState.InitState_CallingOnInit);
3.    markProjectedViewsForCheck(view);
4.    Services.updateDirectives(view, CheckType.CheckAndUpdate);
5.    execEmbeddedViewsAction(view, ViewAction.CheckAndUpdate);
6.    execQueriesAction(
            view, NodeFlags.TypeContentQuery, NodeFlags.DynamicQuery, CheckType.CheckAndUpdate);
7.    let callInit = shiftInitState(
            view, ViewState.InitState_CallingOnInit, ViewState.InitState_CallingAfterContentInit);
8.    callLifecycleHooksChildrenFirst(
            view, NodeFlags.AfterContentChecked | (callInit ? NodeFlags.AfterContentInit : 0));

9.    Services.updateRenderer(view, CheckType.CheckAndUpdate);

10.    execComponentViewsAction(view, ViewAction.CheckAndUpdate);
11.    execQueriesAction(
            view, NodeFlags.TypeViewQuery, NodeFlags.DynamicQuery, CheckType.CheckAndUpdate);
12.    callInit = shiftInitState(
            view, ViewState.InitState_CallingAfterContentInit, ViewState.InitState_CallingAfterViewInit);
13.    callLifecycleHooksChildrenFirst(
            view, NodeFlags.AfterViewChecked | (callInit ? NodeFlags.AfterViewInit : 0));

14.   if (view.def.flags & ViewFlags.OnPush) {
          view.state &= ~ViewState.ChecksEnabled;
14.   }
15.   view.state &= ~(ViewState.CheckProjectedViews | ViewState.CheckProjectedView);
16.   shiftInitState(view, ViewState.InitState_CallingAfterViewInit, ViewState.InitState_AfterInit);
      }

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.

export const enum ViewState {
  BeforeFirstCheck = 1 << 0,
  FirstCheck = 1 << 1,
  Attached = 1 << 2,
  ChecksEnabled = 1 << 3,
...
  CatDetectChanges = Attached | ChecksEnabled,
➥CatInit = BeforeFirstCheck | CatDetectChanges | InitState_BeforeInit
}

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:

  Attached = 1 << 2,
  ChecksEnabled = 1 << 3,
  Destroyed = 1 << 7,

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.

export enum ChangeDetectorStatus {
  CheckOnce,

  Checked,

  CheckAlways,

  Detached,

  Errored,

  Destroyed,
}

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:

export interface Component extends Directive {
  changeDetection?: ChangeDetectionStrategy;

  viewProviders?: Provider[];

  moduleId?: string;

  templateUrl?: string;

  template?: string;

  styleUrls?: string[];

  styles?: string[];

  animations?: any[];

  encapsulation?: ViewEncapsulation;

  interpolation?: [string, string];

  entryComponents?: Array<Type<any>|any[]>;

  preserveWhitespaces?: boolean;
}

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.

export const Component: ComponentDecorator = makeDecorator(
    'Component', (c: Component = {}) => ({changeDetection: ChangeDetectionStrategy.Default, ...c}),
    Directive, undefined,
    (type: Type<any>, meta: Component) => (R3_COMPILE_COMPONENT || (() => {}))(type, meta));

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:

@Component({
  changeDetection: ChangeDetectionStrategy.OnPush
  ...
})
export class AppComponent {...}

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.

@Component({
  selector: 'my',
  template: `
    {{count}}
  `
})
export class MyComponent {
  @Input()
  count:string
}

@Component({
  template: `
    <my [count]="parentCount"></my>
    <button (click)="clickMe()">Click</button>
  `
})
export class AppComponent {
  parentCount=0
  clickMe() {
    this.parentCount++
  }
}

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:

@Component({
  ...
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class AppComponent {...}

The component factory will look like this:

export function View_AppComponent_0(_l) {
    return core.viewDef(1, [
...
    ], /** updateDirectives */, /** updateRenderer */);
}

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:

@Component({
  ...
  template:`
    {{count}}
    <button (click)="up()">Click</button>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class DOMComponent{
  count = 0
  up() {
    this.count++
  }
}

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.

@Component({
  ...
  template:`
    {{count}}
    <button (click)="up()">Click</button>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class DOMComponent{
  count = 0
  constructor() {
    setTimeout(()=> this.count++,0)
  }
  up(){this.count++}
}

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:

export function View_DOMComponent_0(_l) {
    return i1.ɵvid(0, [
        (_l()(), i1.ɵted(0, null, ["  "])),
        (_l()(), i1.ɵeld(1, 0, null, null, 1, "button", [], null, 
        /** outputs*/
        [
          [null, "click"]
        ], 
        /** handleEvent */
        (_v,en,$event)=>{
          var _co = _v.component
          if("click" == en) {
           _co.up() 
          }
        }, null, null)),
        (_l()(), i1.ɵted(-1, null, [" Click "])),
    ], null, (_ck,_v)=>{
      var curr = _v.component.count
      _ck(_v,0,curr)
    });
}

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.

export function listenToElementOutputs(view: ViewData, compView: ViewData, def: NodeDef, el: any) {
  for (let i = 0; i < def.outputs.length; i++) {
    const output = def.outputs[i];
    const handleEventClosure = renderEventHandlerClosure(
        view, def.nodeIndex, elementEventFullName(output.target, output.eventName));
...
    const disposable =
        <any>listenerView.renderer.listen(listenTarget || el, output.eventName, handleEventClosure);
    view.disposables ![def.outputIndex + i] = disposable;
  }
}

function renderEventHandlerClosure(view: ViewData, index: number, eventName: string) {
  return (event: any) => dispatchEvent(view, index, eventName, event);
}

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

export function dispatchEvent(
    view: ViewData, nodeIndex: number, eventName: string, event: any): boolean|undefined {
    ...
    markParentViewsForCheck(startView);
    return Services.handleEvent(view, nodeIndex, eventName, event);
    ...
}

will call markParentViewsForCheck(startView); and eventually call our handleEvent function.

The markParentViewsForCheck function

export function markParentViewsForCheck(view: ViewData) {
  let currView: ViewData|null = view;
  while (currView) {
    if (currView.def.flags & ViewFlags.OnPush) {
      currView.state |= ViewState.ChecksEnabled;
    }
    currView = currView.viewContainerParent || currView.parent;
  }
}

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:

function callViewAction(view: ViewData, action: ViewAction) {
...
    case ViewAction.CheckAndUpdate:
      ...
        if ((viewState & ViewState.CatDetectChanges) === ViewState.CatDetectChanges) {
          checkAndUpdateView(view);
        }
        ...
}

Then after that it will be disabled:

export function checkAndUpdateView(view: ViewData) {
...
  if (view.def.flags & ViewFlags.OnPush) {
    view.state &= ~ViewState.ChecksEnabled;
  }
  ...
}

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:

@Component({
  ...
  template: `{{count}}`,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ZComponent{
  count = 0
  constructor(private cd: ChangeDetectorRef){
    setTimeout(()=>{count++; this.cd.detectChanges()}),1000)
  }
}

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

  detectChanges(): void {
...
      Services.checkAndUpdateView(this._view);
...
}

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:

@Component({
  selector: 'my-i',
  template:`
  {{iHuman.name}}
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class IComponent{
  @Input()
  iHuman: any
}

and on the parent component, we have this:

@Component({
  selector: 'app-parent',
  template: `
    <my-i [iHuman]="pHuman"></my-i>
    <button (click)="incrId()">Incr. Id</button>
    <button (click)="chgName()">Change Name</button>
  `
})
export class PComponent{
  pHuman:any = {
    id:0,
    name: "Nnamdi"
  }
  incrId(){
    this.pHuman.id++
  }
  chgName(){
    this.pHuman.name = "Chidume"
  }
}

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:

app-root
  |
  V
app-parent
  |
  V
my-i [OnPush and deactivated]

CD runs from the top app-root down to my-i. Next, let's look at the factory generated for the PComponent:

<my-i [iHuman]="pHuman"></my-i>
<button (click)="incrId()">Incr. Id</button>
<button (click)="chgName()">Change Name</button>
  |
  V
export function View_PComponent_0(_l) {
    return i1.ɵvid(0, [
      (_l()(), i1.ɵeld(0, 0, null, null, 1, "my-i", [], null, null, null, View_IComponent_0, RenderType_IComponent)), 
    i1.ɵdid(1, 49152, null, 0, i2.IComponent, [], 
    // bindings
    [
      iHuman: [0, "iHuman"]
    ], null), 
    (_l()(), i1.ɵeld(2, 0, null, null, 1, "button", [], null, [
        [null, "click"]
    ], function(_v, en, $event) {
        var ad = true;
        var _co = _v.component;
        if (("click" === en)) {
            var pd_0 = (_co.incrId() !== false);
            ad = (pd_0 && ad);
        }
        return ad;
    }, null, null)), 
    i1.ɵted(-1,["Incr. Id"]),
    (_l()(), i1.ɵeld(4, 0, null, null, 1, "button", [], null, [
        [null, "click"]
    ], function(_v, en, $event) {
        var ad = true;
        var _co = _v.component;
        if (("click" === en)) {
            var pd_0 = (_co.chgName() !== false);
            ad = (pd_0 && ad);
        }
        return ad;
    }, null, null)), 
    i1.ɵted(-1,["Change Name"])
    ], 
    // updateDirectives
    function (_ck, _v) {
      var _co = _v.component;
      var currVal_0 = _co.pHuman;
      _ck(_v, 1, 0, currVal_0);
    }
    , /** updateRenderer */null);
}

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,

export function checkAndUpdateDirectiveInline(...): boolean {
  const providerData = asProviderData(view, def.nodeIndex);
  const directive = providerData.instance;
  let changed = false;
  let changes: SimpleChanges = undefined !;
  const bindLen = def.bindings.length;
  if (bindLen > 0 && checkBinding(view, def, 0, v0)) {
    changed = true;
    changes = updateProp(view, providerData, def, 0, v0, changes);
  }
  ...

The value to be updated is checked against the value in the directive's class in the checkBinding fucntion.

export function checkBinding(
    view: ViewData, def: NodeDef, bindingIdx: number, value: any): boolean {
  const oldValues = view.oldValues;
  if ((view.state & ViewState.FirstCheck) ||
      !looseIdentical(oldValues[def.bindingIndex + bindingIdx], value)) {
    return true;
  }
  return false;
}

  |
  V
export function looseIdentical(a: any, b: any): boolean {
  return a === b || typeof a === 'number' && typeof b === 'number' && isNaN(a) && isNaN(b);
}

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.

function updateProp(
    view: ViewData, providerData: ProviderData, def: NodeDef, bindingIdx: number, value: any,
    changes: SimpleChanges): SimpleChanges {
  if (def.flags & NodeFlags.Component) {
    const compView = asElementData(view, def.parent !.nodeIndex).componentView;
    if (compView.def.flags & ViewFlags.OnPush) {
      compView.state |= ViewState.ChecksEnabled;
    }
  }
...
}

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:

let obj= { name: "Chidume" , id:2}
// creates variable obj on the stack and stores { name: "Chidume" } on the heap. 
//obj has the memory address where { name: "Chidume" } is stored on the heap
// obj == #001 (let's take #001 is the number of the memory cell)

obj.name = "David"
// This refers to the memory #001 and changes the `name` property.

var o = obj // creates variable `o` on the stack and points it to #001.

console.log(o === obj) // true
// `===` checks if the memory address pointed to by `o` is equal to the memory address pointed to by `obj`.

obj = { name: "David" }
// This creates `{ name: "David" }` on the heap and points obj to the memory address number #001
// obj changes from #001 to #002

console.log(o === obj) // false
// It will print `false` because, now `obj` points to #002 and o points to #001
console.log(obj)

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:

@Component({
  selector: 'app-parent',
  template: `
    <my-i [iHuman]="pHuman"></my-i>
    <button (click)="incrId()">Incr. Id</button>
    <button (click)="chgName()">Change Name</button>
  `
})
export class PComponent{
  pHuman:any = {
    id:0,
    name: "Nnamdi"
  }
  incrId(){
    const id = this.pHuman.id + 1
    this.pHuman = {...this.pHuman, id}
  }
  chgName(){
    const name = "Chidume"
    this.pHuman = {...this.pHuman, name}
  }
}

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.

export abstract class ChangeDetectorRef {
  abstract markForCheck(): void;

  abstract detach(): void;

  abstract detectChanges(): void;

  abstract checkNoChanges(): void;

  abstract reattach(): void;
}

Now, let's explore its methods:

markForCheck

This method is used to enable ChecksEnabled for OnPush parent components of an OnPush component.

markForCheck(): void { markParentViewsForCheck(this._view); }
  |
  V
export function markParentViewsForCheck(view: ViewData) {
  let currView: ViewData|null = view;
  while (currView) {
    if (currView.def.flags & ViewFlags.OnPush) {
      currView.state |= ViewState.ChecksEnabled;
    }
    currView = currView.viewContainerParent || currView.parent;
  }
}

Let's see where this method comes in handy.

@Component({
  selector: 'my-i',
  template:`
  {{count}}
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class IComponent{
  count = 0
  constructor() {
    setTimeout(()=>{this.count++},1000)
  }
}

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.

@Component({
  selector: 'my-i',
  template:`
  {{count}}
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class IComponent{
  count = 0
  constructor(private cd: ChangeDetectorRef) {
    setTimeout(()=>{
      this.count++;
      this.cd.markForCheck()
      },1000)
  }
}

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.

@Component({
  selector: 'my-i',
  template:`
  {{count}}
  <button (click)="showCount()">ShowCount</button>
  `
})
export class IComponent{
  count = 0
  constructor(private cd: ChangeDetectorRef) {
    this.cd.detach()
    setTimeout(()=>{this.count++},1000)
  }
  showCount() {
    this.cd.detectChanges()
  }
}

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.

@Component({
  selector: 'my-i',
  template:`
  {{count}}
  `
})
export class IComponent{
  count = 0
  constructor(private cd: ChangeDetectorRef) {
    this.cd.detach()
    setTimeout(()=>{
      this.count++
      if(this.count == 100){
        this.cd.reattach()
      }
    },1000)
  }
}

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