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 TurnonMicrotaskEmpty
: 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 oneonMicrotaskEmpty
Already 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 pass
onMicrotaskEmpty
To 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?