This article has participated in the activity of “New person creation Ceremony”, and started the road of digging gold creation together.

As a front-end framework designed for “big front-end projects,” Angular has a lot of design to learn from, and this series focuses on how those designs and features work. This article focuses on the design and implementation of NgZone in Angular.

In our last article, we introduced zone.js, which solves many of the context problems associated with asynchronous Javascript programming.

NgZone incorporates capabilities applicable to the Angular framework based on zone.js. Performance optimization for Angular data change detection (dirty check) relies on NgZone design.

NgZone

While zone.js can monitor all the states of synchronous and asynchronous operations, Angular also provides a service called NgZone.

NgZone is an injectable service for performing work inside and outside Angular regions, optimizing performance for asynchronous tasks that do not require Angular to handle UI updates or error handling.

NgZone design

Let’s look at the NgZone implementation:

export class NgZone {
  readonly hasPendingMacrotasks: boolean = false;
  readonly hasPendingMicrotasks: boolean = false;
  readonly isStable: boolean = true;
  readonly onUnstable: EventEmitter<any> = new EventEmitter(false);
  readonly onMicrotaskEmpty: EventEmitter<any> = new EventEmitter(false);
  readonly onStable: EventEmitter<any> = new EventEmitter(false);
  readonly onError: EventEmitter<any> = new EventEmitter(false);
  constructor({
    enableLongStackTrace = false,
    shouldCoalesceEventChangeDetection = false,
    shouldCoalesceRunChangeDetection = false
  }){...// Create a subregion in the current region as an Angular region
    forkInnerZoneWithAngularBehavior(self);
  }
  // Whether it is in the Angular region
  static isInAngularZone(): boolean {
    return Zone.current.get('isAngularZone') = = =true;
  }
  // Execute the fn function synchronously in the Angular region and return the value returned by the function
  // Running with run re-enters the Angular zone for tasks executed outside the Angular zone
  run<T>(fn: (. args:any[]) = >T, applyThis? :any, applyArgs? :any[]): T {
    return (this as any as NgZonePrivate)._inner.run(fn, applyThis, applyArgs);
  }
  // Execute the fn function synchronously as a task in the Angular region and return the value returned by the function
  runTask<T>(fn: (. args:any[]) = >T, applyThis? :any, applyArgs? :any[], name? :string): T {
    const zone = (this as any as NgZonePrivate)._inner;
    const task = zone.scheduleEventTask('NgZoneEvent: ' + name, fn, EMPTY_PAYLOAD, noop, noop);
    try {
      return zone.runTask(task, applyThis, applyArgs);
    } finally{ zone.cancelTask(task); }}// Same as run, except that synchronization errors are caught and forwarded through onError instead of being rethrown
  runGuarded<T>(fn: (. args:any[]) = >T, applyThis? :any, applyArgs? :any[]): T {
    return (this as any as NgZonePrivate)._inner.runGuarded(fn, applyThis, applyArgs);
  }
  // Synchronously executes the fn function outside the Angular region and returns the value returned by the function
  runOutsideAngular<T>(fn: (. args:any[]) = > T): T {
    return (this as any asNgZonePrivate)._outer.run(fn); }}Copy the code

NgZone adds a layer of encapsulation on top of zone.js and forks to create sub-regions for Angular regions:

function forkInnerZoneWithAngularBehavior(zone: NgZonePrivate) {...// Create a subregion, an Angular region
  zone._inner = zone._inner.fork({
    name: 'angular'.properties: <any> {'isAngularZone': true},... }); }Copy the code

In addition, the property isStable, which indicates that there are no microtasks or macro tasks, has been added to NgZone for state detection. In addition, NgZone defines four events:

  • onUnstable: Notifies code when it enters the Angular Zone, first triggered on the VM Turn
  • onMicrotaskEmpty: Notifies the current VM Turn that no more microtasks are queued. This is Angular’s cue for change detection, which may queue up more microtasks (this event can be triggered multiple times per VM flip)
  • onStable: The last oneonMicrotaskEmptyAlready running and no more microtasks, which means VM steering is about to be abandoned (this event is called only once)
  • onError: Notification of transmission error

As we saw in the previous section, zone.js handles most of the asynchronous apis, such as setTimeout(), promise.then (), and addEventListener(). The run() method of the NgZone service allows functions to be executed in angular Zone for third-party apis that zone.js cannot handle.

With Angular Zone, all asynchronous operations in a function automatically trigger change detection at the correct time.

Automatically triggers change detection

An Angular Zone is created to automatically trigger change detection when the NgZone meets the following criteria:

  • When performing synchronous or asynchronous functions (zone.js has built-in change detection, it will eventually passonMicrotaskEmptyTo trigger)
  • There are no planned Microtasks (onMicrotaskEmpty)

The onMicrotaskEmpty condition triggers the listener, and the detection logic is located in ApplicationRef:

@Injectable(a)export class ApplicationRef {...constructor(
      private _zone: NgZone, private _injector: Injector, private _exceptionHandler: ErrorHandler,
      private _componentFactoryResolver: ComponentFactoryResolver,
      private _initStatus: ApplicationInitStatus) {
    // Change detection is triggered when Microtask is empty
    this._onMicrotaskEmptySubscription = this._zone.onMicrotaskEmpty.subscribe({
      next: () = > {
        this._zone.run(() = > {
          // Tick is the logic for change detection, which will re-evaluate and render the template
          this.tick(); }); }}); . }Copy the code

Let’s see when the onMicrotaskEmpty event is triggered:

function checkStable(zone: NgZonePrivate) {
  if (zone._nesting == 0&&! zone.hasPendingMicrotasks && ! zone.isStable) {try {
      zone._nesting++;
      zone.onMicrotaskEmpty.emit(null);
    } finally {
      zone._nesting--;
      if(! zone.hasPendingMicrotasks) {try {
          zone.runOutsideAngular(() = > zone.onStable.emit(null));
        } finally {
          zone.isStable = true;
        }
      }
    }
  }
}
Copy the code

When the onInvokeTask and onInvoke hooks are fired, the microtask queue can change, so Angular must run a check every time the hook is fired. In addition, the onHasTask hook is also used to perform checks because it tracks changes to the entire queue:

function forkInnerZoneWithAngularBehavior(zone: NgZonePrivate) {
  const delayChangeDetectionForEventsDelegate = () = > {
    / / delayChangeDetectionForEvents internal call checkStable ()
    delayChangeDetectionForEvents(zone);
  };
  zone._inner = zone._inner.fork({
    ...
    onInvokeTask:
        (delegate: ZoneDelegate, current: Zone, target: Zone, task: Task, applyThis: any.applyArgs: any) :any= >{...// Perform a test
          delayChangeDetectionForEventsDelegate();
        },

    onInvoke:
        (delegate: ZoneDelegate, current: Zone, target: Zone, callback: Function.applyThis: any, applyArgs? :any[], source? :string) :any= >{...// Perform a test
          delayChangeDetectionForEventsDelegate();
        },

    onHasTask:
        (delegate: ZoneDelegate, current: Zone, target: Zone, hasTaskState: HasTaskState) = >{...if (current === target) {
            // Check only the tasks in the current region
            if (hasTaskState.change == 'microTask') {
              zone._hasPendingMicrotasks = hasTaskState.microTask;
              updateMicroTaskStatus(zone);
              // Trace the MicroTask queue and checkcheckStable(zone); }... }}}); }Copy the code

By default, all asynchronous operations are within the Angular Zone, which automatically triggers change detection.

Another common case is when we don’t want to trigger change detection (for example, we don’t want events like Scroll to do change detection too frequently and cause performance problems), and we can use NgZone’s runOutsideAngular() method.

Zone.js helps Angular know when to trigger change detection and keeps developers focused on application development. By default, zone.js is loaded and works without additional configuration. If you want to choose to trigger change detection yourself, you can do so by disabling zone.js.

conclusion

This article describes how NgZone encapsulates zone.js so that all asynchronous operations within Angular zone functions automatically trigger change detection at the correct time.

You can use NgZone’s runOutsideAngular() method to reduce change detection as needed, or you can implement your own change detection logic by disabling zone.js.

reference

  • Angular-NgZone
  • Do you still think that NgZone (zone.js) is required for change detection in Angular?