Understanding Zones and Change Detection in Angular
Zones and change detection are an important part of Angular, and so it is important to our Ionic applications. I figured I understood what was happening at a surface level with change detection, but didn't really get it. I decided to do a deep dive into some research to try and clarify my understanding, and this article is my attempt at summarising what I discovered by reading lots of articles by people smarter than me.
This article aims to provide a simple introduction to Zones, and how they are important to change detection in Angular. I originally researched and wrote this article in 2016 (which is, like, a pretty long time ago by web standards) but all of this is still relevant today (I checked, and I'm at least moderately smarter today).
Outline
What is Change Detection?
Pascal Precht describes the task of change detection eloquently here:
The basic task of change detection is to take the internal state of a program and make it somehow visible to the user interface.
Another name for change detection is dirty checking. Compared to AngularJS (i.e. pre-Angular 2), change detection in Angular can seem like magic. Generally, something changes and Angular just somehow knows about it and updates the view accordingly. Of course, it isn't magic, there's a perfectly logical explanation that we are going to explore in this article.
The TL;DR (too long; didn't read) version I was able to come up with of how change detection in Angular works is this:
Angular runs inside of its own special zone called NgZone. Running inside of a "zone" allows Angular to detect when asynchronous tasks – things that can alter the internal state of an application, and therefore its views – start and finish. Since these asynchronous tasks are the only thing that are going to cause our views to change, by detecting when they are executed Angular knows that a view may need to be updated.
...or to put it a little less technically:
Intervals like setTimeout
and setInterval
, HTTP requests, and events might cause state changes after the application has finished rendering initially. Angular patches these things to notify it when any of them are executed so it knows to check if the view needs to be updated.
We can go into a lot more detail than that though, and I think understanding those details to some degree helps solidify the concept. So let's continue.
What are Zones?
I mentioned above that Angular implements its own special zone called NgZone. This special zone extends the basic functionality of a zone to facilitate change detection. But what the heck is a zone anyway?
A zone is not a concept that is specific to Angular. Zones can be added to any Javascript application with the inclusion of the Zone.js library, and the project itself describes zones as:
...an execution context that persists across async tasks. You can think of it as thread-local storage for JavaScript VMs.
I think this is a good analogy, but there are a few concepts mentioned in that definition which, depending on your level of knowledge of Javascript and programming in general, you may or may not understand. So let's expand on that a bit.
First of all, let's get the easy ones out of the way. An asynchronous task is a task that runs outside of the normal flow of the program – the program will go on executing without waiting for the asynchronous tasks to finish. When the async task does finish the application can handle it through the use of something like a callback or a promise. All asynchronous tasks in Javascript are added to an event queue, and tasks in the event queue are executed by the event loop when there is time (i.e. when the stack is empty, the event loop will push the task onto the stack):
The image above gives a visualisation of this. Normal (synchronous) function calls in a program are added to the stack, which are then executed from the top down. Any asynchronous function calls are added to the event queue. Once the stack is empty, the tasks in the queue can begin being processed.
A JavaScript Virtual Machine (VM), or a JavaScript engine, is a program that executes JavaScript code. Different browsers build different engines that interpret and run JavaScript code differently, like Google's V8 engine and Apple's Nitro. To us, it's mostly the same, but different engines will have (slightly) different interpretations of syntax and performance.
Thread-local storage is a programming concept that essentially creates global memory for a specific thread. A thread is a bit of code that can run independently of other bits of code that may be running at the same time. So a thread is somewhat similar to an asynchronous task in Javascript, in that it doesn't run in line with other tasks. This is important for an operating system (and other things) because it allows the computer to multi-task. You probably have 5
or more applications open on your computer right now, running a bunch of different processes. Threading allows the sharing of processing power on your computer, switching back and forth between different threads as required, creating the illusion that your computer is doing multiple things at once. Really, your computer is still only doing one thing at a time (if you have a single processor), but it is actually doing very, very tiny single bits of work at a time through the use of threads.
Since different threads – which execute some logic – run at effectively the same time, it creates the perfect environment for conflicts and race conditions. If a thread wanted to store some global variable, and another thread accesses the same variable, the end result of the program may depend on the times at which each thread executed – which is unpredictable and unrepeatable. Thread-local storage allows a thread to have its own global space to store variables, independent of another thread.
Putting those definitions together, we could change that original analogy to say:
You can think of zones as a way for the Javascript engine to create spaces to run code independently of other bits of code.
So, why are zones – these independent execution contexts – important to change detection? The important part is that the zones persist across async tasks, so an asynchronous task will run within the same zone that it was created. Zones don't just allow us to run bits of code in their own happy little worlds, they also allow us to hook into them to detect when asynchronous tasks start and finish.
I think Pascal Precht's article on Understanding Zones does a great job at explaining this, so I hope he doesn't mind me borrowing his example here. If you consider the following code:
function doSomething() {
console.log('Async task');
}
// start timer
start = timer();
foo();
setTimeout(doSomething, 2000);
bar();
baz();
// stop timer
time = timer() - start;
We execute a series of functions, and we have a timer set up to detect how long it takes to execute. The issue in the code above is that we use a setTimeout
which creates an asynchronous task that will be added to the event queue. The code doesn't wait around for the 2
seconds it takes for that task to finish, instead it will go on executing bar()
and then baz()
and then it will stop the timer – all before the doSomething
task has had a chance to run. The execution time that will be reported won't really be accurate (if we want to consider the 2
seconds that it takes for doSomething
to trigger).
Imagine this is our Ionic application – if we set up our view once the code above finishes executing, how is it supposed to know that something may have changed once doSomething
actually does finish, unless we were to manually make some call from doSomething
to notify our application that a change is required.
Now take a look at another example from the same article:
var myZoneSpec = {
beforeTask: function () {
console.log('Before task');
},
afterTask: function () {
console.log('After task');
},
};
var myZone = zone.fork(myZoneSpec);
myZone.run(main);
We are creating a new zone by forking the parent zone and providing it with a "spec". This spec uses the beforeTask
and afterTask
hooks, which can detect when an asynchronous tasks starts and finishes in a zone. We could then use those hooks to accurately time how long all of the code takes to complete, including the asynchronous doSomething
function, because doSomething
will trigger these hooks. Once we have our special zone created, we run the same program inside of myZone.run
. This is essentially what Angular does to create its custom NgZone
zone and set up change detection (which we will talk more about in just a moment).
Any time that a task is executed within the context of a zone, we can detect when it starts and when it finishes. This is very powerful.
How Does Change Detection Work?
By now, you might have a pretty decent idea of how change detection works. Angular has its own zone – its own execution context – and it can detect when any asynchronous task starts or finishes within that zone. Any task that is executed within Angular's zone will trigger a change. That's an important concept to understand, because anything executed outside of Angular's zone won't trigger a change.
We're going to go into a little bit more depth on how this works, but still keep it pretty surface level. If you want a very low-level explanation of how change detection works in Angular, I would recommend reading Angular 2 Change Detection Explained.
When we first run our application our code will start executing. The root component is created and bootstrapped, our components are created, all of our constructor functions in our components will execute their code and so on. Eventually, once everything settles down (the stack is empty), our application reaches a nice resting state. Everything has been determined, and our views can reflect that.
An application that never changes is pretty boring though, in fact, one could argue it's not an application at all! Once that initial state of the application is determined, there are a few ways that the state can change – all of which are caused by asynchronous tasks, which can be:
- Events like
(click)
- Http Requests like
http.post
- Timers like
setTimeout
A user clicking a button can happen at any time, and that button click may cause a change to a view. Maybe we have some code set up that disables the button once the user clicks it, that is going to require the view to update. An HTTP request could be triggered immediately by our application, or it could be triggered by a user clicking on a button, either way it could take anywhere from 10 milliseconds to 10 seconds (to never) to complete, and it could also cause a change to a view.
Let's take a look at the following example:
private myTitle = "Hello";
constructor(){
setTimeout(() => {
this.myTitle = "Goodbye!";
}, 5000);
}
<ion-title>{{myTitle}}</ion-title>
In this example we initially set the member variable myTitle
to Hello
. Once our application has finished loading and reaches that first "resting state", myTitle
will be Hello
and the template will know to display it as the <ion-title>
.
However, we have a timer here, and after 5
seconds it is going to alter the state of the application by changing myTitle
to Goodbye!
. The problem is: how is Angular supposed to know about this? Well, we already know it uses zones to figure this out, but how?
As I mentioned, NgZone is a special type of zone created by Angular. It forks its parent zone, which allows it to set up its own functionality on that zone. This extra functionality includes adding an onTurnDone
event (similar to afterTask
that we discussed before), which triggers when Angular's zone finishes processing the current turn – a turn is what happens when the event loop pushes a task from the event queue (an asynchronous task) to the stack.
If we take a look at the following code example from Zones in Angular 2:
ObservableWrapper.subscribe(this.zone.onTurnDone, () => {
this.zone.run(() => {
this.tick();
});
});
tick() {
// perform change detection
this.changeDetectorRefs.forEach((detector) => {
detector.detectChanges();
});
}
We can see that every time a "turn" completes in Angular's zone it triggers the tick
function. If there were some asynchronous tasks, once the stack is emptied they would be added to the stack and executed – once those have finished executing Angular would be notified that another turn has completed. This tick
function then triggers change detection for each component every time a "turn" completes. In effect, every time an asynchronous function finishes executing, Angular triggers tick
which checks for changes.
By default, Angular will check every single component starting from the root component when a change is detected. Depending on your application this may or may not be fine - if your application is very large, then this means a lot of work needs to be performed for each change detection cycle. Although it is not the focus of this article, it is worth keeping in mind that we can use the OnPush
change detection strategy to prevent Angular from needing to check every single component in our component tree. We talk more about these concepts in the High Performance Applications module.
The main thing to remember is that in order for something to trigger change detection it needs to be executed within Angular's zone. If you've been developing Ionic applications for a little while now you may have even used NgZone directly like this:
this.zone.run(() => {
// some code
});
This forces whatever is inside of the handler for the run
method to run inside of Angular's zone, which will ensure that it triggers change detection. Similarly, we could also run something outside or Angular 2's zone:
this.zone.runOutsideAngular(() => {
// do something
// reenter Angular zone
this.zone.run(() => {
// back in Angular town
});
});
This would instead run the code in Angular's parent zone, the zone that Angular's zone is forked from, so that it won't trigger change detection (which may be useful for performance reasons).
Summary
There's plenty more to know about change detection in Angular, and if you want to dive even deeper I'll link some resources below for you to take a look at. This article should give you a broad enough understanding to be comfortable with change detection and zones though. In reality, you're probably not going to need to implement anything we've discussed here yourself, but if you are having trouble with change detection it will be handy to be able to try and figure out why it is happening.