I always wondered what a zone is in Angular and why it is needed.
I am sure you know that zones are provided by the zone.js library, which is included by default in Angular (below 21 versions), and that zones are mainly used to provide automatic change detection in Angular applications. This means that changes in the application state will be automatically reflected in the DOM.
This sounded very abstract to me, and while I understood the purpose, it was a mystery what a zone really was.
We can think of a zone as just a label that is attached to async tasks in the browser. We attach the label/zone during async task registration, such as when calling setTimeout, addEventListener, or Promise.then. When the registered callback is later called i.e. placed on the call stack, our attached label is still there, and we know from which label/zone it was registered.
For example:
let currentZone = 'root'; // this is our zone or label; it is a global variable
const log = (msg) => {
console.log(`${currentZone}: ${msg}`); // currentZone is used here
}
const func1 = () => {
currentZone = 'Angular'; // we entered the Angular zone
setTimeout(() => log("from func1"), 10);
}
const func2 = () => {
currentZone = 'other'; // we entered the other zone
setTimeout(() => log("from func2"), 20);
}
func1();
func2();If you execute this code in the browser, both setTimeout callbacks will log ‘other’ as the currentZone. In order for both callbacks to preserve their zones, we need to pass this information to the callbacks themselves.
While it is easy to move the currentZone reassignment directly into the setTimeout callback in our small example, it is impossible for a library creator to do this in a library consumer’s code. Otherwise, the consumer would be responsible for updating it every time in all callbacks; hence, it would no longer be automatic.
This is why zone.js monkey patches async operations in the browser.
When we call a monkey patched, zone aware setTimeout or addEventListener, the callback is captured and a label is attached.
In fact, zone.js wraps our callback in a ZoneTask. It is a class, and during the registration or scheduling of the callback, a ZoneTask instance is created, and the callback with its zone are saved in the ZoneTask object. The ZoneTask later invokes the callback within the attached zone.
This happens automatically.
We can also manually invoke some operations from a particular zone. If you have ever called ngZone.run(() => {}), it is the same thing. Angular itself calls ngZone.run() in many places.
The NgZone object contains other methods too, such as runOutsideAngular.
Calling ngZone.run enters the Angular zone and then calls our callback, while ngZone.runOutsideAngular enters the <root> zone and executes the callback from there.
Example:
const angularZone = Zone.current.fork({name: 'Angular'});
const otherZone = Zone.current.fork({name: 'Other'});
const log = (msg) => {
console.log(`${Zone.current.name}: ${msg}`); // Zone.current.name is used in the log
}
angularZone.run(() => {
setTimeout(() => log("logging from angular"), 10);
})
otherZone.run(() => {
setTimeout(() => log("logging from other"), 20);
})If you execute this code in the browser console, you will see that both setTimeout callbacks preserve their zones correctly. (Make sure zone.js is loaded before executing it.)
This is all good, but what benefit do we get from all this?
Zones are attached to async callbacks and preserved, so Angular can trigger change detection for callbacks registered in the Angular zone and skip it for other zones.
This is done with the help of hooks.
When forking from the current zone above, we passed an object as an argument: {name: 'Angular'}.
Angular passes additional properties and hooks in this object:
// This is simplified
{
name: 'angular',
properties: {'isAngularZone': true},
onInvokeTask: ()=>{
zone.onMicrotaskEmpty.emit(null);
},
onInvoke: ()=>{
zone.onMicrotaskEmpty.emit(null);
},
onHasTask: ()=>{
zone.onMicrotaskEmpty.emit(null);
},
onHandleError: ()=>{
// error handling goes here
},
}Those methods are called by zone.js when our tasks are invoked, as the name suggests.
This is where Angular emits the onMicrotaskEmpty event and notifies ApplicationRef to call its tick method to trigger change detection.
That is it. I hope it helped.
Thank you for reading.
