Zone.js deep diving
Chapter 1: Execution Context
As an Angular developer, you may know NgZone
, which is a service for executing work inside, or outside of the angular Zone
. You may also know that this NgZone
service is based on a library called zone.js
, but most developers may not directly use the APIs of zone.js
, or know what zone.js
is, so I would like to use several articles to explain zone.js
to you.
My name is Jia Li
. I am a senior software engineer at This Dot Labs, and I have contributed to zone.js
for more than 3 years. Now, I am the code owner of angular/zone.js
package (zone.js had been merged into angular monorepo
), and I am also an Angular collaborator.
What is Zone.js
Zone.js
is a library created by Brian Ford in 2010 and is inspired by Dart. It provides a concept called Zone
, which is an execution context that persists across async tasks.
A Zone
can:
- Provide execution context that persist across async tasks.
- Intercept async task, and provide life cycle hooks.
- Provide centralized error handler for async tasks.
We will discuss those topics one by one.
Execution Context
So what is Execution Context
? This is a fundamental term in Javascript. Execution Context
is an abstract concept that holds information about the environment within the current code being executed. The previous sentence may be a little difficult to understand without context, so let's use some code samples to explain it. For better understanding of Execution Context/Scope
, please refer to this great book from getify
- Global Context
const globalThis = this;
let a = 0;
function testFunc() {
let b = 0;
console.log('this in testFunc is:', this === globalThis);
}
testFunc();
So in this first example, we have a global execution context
, which will be created before any code is created. It needs to know it's scope
, which means the execution context
needs to know which variables and functions it can access. In this example, the global execution context can access variable a
. Then, after the scope is determined, the Javascript engine will also determine the value of this
. If we run this code in Browser
, the globalThis
will be window
, and it will be global
in NodeJS
.
Then, we execute testFunc
. When we are going to run a new function, a new execution context
will be created, and again, it will try to decide the scope
and the value of this
. In this example, the this
in the function testFunc
will be the same with globalThis
, because we are running the testFunc
without assigning any context object
by using apply/call
. And the scope
in testFunc
will be able to access both a
and b
.
This is very simple. Let's just see another example to recall the Javascript 101.
const testObj = {
testFunc: function() {
console.log('this in testFunc is:', this);
}
};
// 1. call testFunc with testObj
testObj.testFunc();
const newTestFunc = testObj.testFunc;
// 2. call newTestFunc who is referencing from testObj.testFunc
newTestFunc();
const newObj = {};
// 3. call newTestFunc with apply
newTestFunc.apply(newObj);
const bindObj = {};
const boundFunc = testObj.testFunc.bind(bindObj);
// 4. call bounded testFunc
boundFunc();
boundFunc.apply(somethingElse);
Here, testFunc
is a property of testObj
. We call testFunc
in several ways.
We will not go very deeper about how it works. We just list the results here. Again, please check getify for more details.
- call
testObj.testFunc
,this
will betestObj
. - create a reference
newTestFunc
,this
will beglobalThis
. - call with
apply
,this
will benewObj
. - call
bounded version
,this
will always bebindObj
.
So we can see that this
will change depending on how we call this function. This is a very fundamental mechanism in Javascript.
So, back to Execution Context
in Zone
. What is the difference?
Let's see the code sample here:
const zoneA = // create a new zone ...;
zoneA.run(function() {
// function is in the zone
// just like `this`, we have a zoneThis === zoneA
expect(zoneThis).toBe(zoneA);
setTimeout(function() {
// the callback of async operation
// will also have a zoneThis === zoneA
// which is the zoneContext when this async operation
// is scheduled.
expect(zoneThis).toBe(zoneA);
});
Promise.resolve(1).then(function() {
// all async operations will be in the same zone
// when they are scheduled.
expect(zoneThis).toBe(zoneA);
});
});
So, in this example, we created a zone
(we will talk about how to create a zone
in the next chapter). As suggested by the term zone
, when we run a function inside the zone
, suddenly we have a new execution context
provided by zone
. Let's call it zoneThis
for now. Unlike this
, the value of zoneThis
will always equal the zone
, where the functions is being executed in no matter if it is a sync
or an async
operation.
You can also see, in the callback of setTimeout
, that the zoneThis
will be the same value when setTimeout
is scheduled. So this is another principle of Zone. The zone execution context will be kept as the same value as it is scheduled.
So you may also wonder how to get zoneThis
. Of course, we are not inventing a new Javascript keyword zoneThis
, so to get this zone context
, we need to use a static method, introduced by Zone.js
, which is Zone.current
.
const zoneA = // create a new zone ...;
zoneA.run(function() {
// function is in the zone
// just like `this`, we have a Zone.current === zoneA
expect(Zone.current).toBe(zoneA);
setTimeout(function() {
// the callback of async operation
// will also have a Zone.current === zoneA
// which is the zoneContext when this async operation
// is scheduled.
expect(Zone.current).toBe(zoneA);
});
Because there is a Zone execution context
we can share inside a zone
, we can also share some data.
const zoneA = Zone.current.fork({
name: 'zone',
properties: {key: 'sharedData'}
});
zoneA.run(function() {
// function is in the zone
// we can use data from zoneA
expect(Zone.current.get('key')).toBe('sharedData');
setTimeout(function() {
// the callback of async operation
// we can use data from zoneA
expect(Zone.current.get('key')).toBe('sharedData');
});
Execution Context
is the fundamental feature of Zone.js
. Based on this feature, we can monitor/track/intercept the lifecycle of async operations. We will talk about those hooks in the next chapter.