Mind is Software

Ying’s thoughts about software and business

Angular Change Detection

A basic requirement for every Web framework is to synchornize a data model and the Browser DOM. Detecting data model change is a central task. Angular separates updating the application and DOM changes into two distinct phases. The developer is responsible for model changes in one phase and Angular synchronizes the model changes in change detection in another phase.

Angular enforces an unidirectinal data flow from parent to child via @Input() property binding. A child should not change its parent’s state directly. A child can communicate with its parent using @Output() event or via shared service.

1 Basic Concepts

There are two approaches to detect changes: manually and automatically. Though it is possible to manually trigger change detection by calling ApplicationRef.tick(), NgZone.run(callback), ChangeDetectorRef.detectChanges() or ChangeDetectorRef.markForCheck(), most applications depend on Angular’s automatic change detection.

The default change detection strategy is ChangeDetectionStrategy.Default. In default strategy, Angular assumes nothing and runs change detection whenever an operation performed. The operations include XHRs, promises, timers, DOM events (click, keydown, keyup, focus etc). In development build, Angular synchronously checks change twice, one before the DOM updates and one before theAfterViewInit/afterViewCheckedevent, to make sure values are not changed again after the first change detection. It throws anExpressionChangedAfterItHasBeenCheckedError error if it detects change in the second check.

When use ChangeDetectionStrategy.OnPush change detection strategy, Angular runs change detection only in two cases: 1) The @Input reference changes (the === comparison changes), and 2) A DOM event originated from the component or one of its children. It ignores all timers, promises and HTTP events. An exception is that when you use the async pipe to display data from asynchrounous operations like HTTP requests or promises, the async pipe marks the component to be checked by calling the ChangeDetectorRef.markForCheck() method.

It is recommended to use the ChangeDetectionStrategy.OnPush strategy for better performance. Correspondingly, use @Input with immutable objects and the async pipe in components.

2 Change Detection

Angular watches every data model properties and check the value change in change detection cycle. For each component, it tracks the following:

  • Parent component bindings @Input, in ngOnChanges().
  • Self component properties, in ngDoCheck()
  • Computed values, in ngDoCheck()

Angular renders DOM after ngDoCheck(). In development mode, Angular also performs a second change detection cycle to ensure that no further changes are detected. It happens before ngAfterViewInit() and ngAfterViewChecked().

Since Angular uses zone to patch all asynchronous events, no manual triggering of change detection is required for most of the events. To run code without triggering change detection, using NgZone.runOutsideAngular() to run asynchronous code. It is often used to run time-consuming code without triggering change detection. Some third party libiraries such as Google API Client Library(gapi) uses JSONP that is not patched by Zones. Use zone.run(() => {}) to call these apis.

For each component, Angular creates a component view that holds a reference to the associated component class instance. A component view is encapsulated in a ViewRef that is subclass of ChangeDetectorRef. The ChangeDetectorRef has the following definition:

export abstract class ChangeDetectorRef {
  abstract checkNoChanges(): void
  abstract detach(): void
  abstract detectChanges(): void
  abstract markForCheck(): void
  abstract reattach(): void

Change detection starts on the top-most ViewRef that calls change detection recursively for its child views. The change detection process has the following steps:

  1. check and update input properties of child components/directives.
  2. call OnChanges on a child component if its bindings change.
  3. call OnInit and DoCheck on child components. These are called even if a child’s change detection is skipped.
  4. call AfterContentInit and AfterContentChecked lifecycle hooks on child components.
  5. update DOM if properties of the current view component instance changes. This means that the DOM is not updated if the change detection is skipped for a component.
  6. run change detection on child components if they are enabled.
  7. call AfterViewInit and AfterViewChecked lifecycle hooks on child components.

All component views are initialized with ChecksEnabled enabled by default, but for all components that use OnPush strategy change detection is disabled after the first check. Those components will be checked only if their input properties have changed. If a component A has a child component B, following is the order of events:

A: AfterContentInit
A: AfterContentChecked
A: Update A's DOM bindings
  B: AfterContentInit
  B: AfterContentChecked
  B: Update B's DOM bindings
  B: AfterViewInit
  B: AfterViewChecked
A: AfterViewInit
A: AfterViewChecked

The detach() and reattach() method disables and enables ChecksEnabled state only for the current component. The markForCheck() enables ChecksEnabled for all parent components up to the root component.

The detectChanges() runs change detection for the current component view regardless of its state.

The checkNoChanges() ensures that there will be no changes on the current run. It throws an exception if it detects a change.