Skip to content

Zone.js deep diving - Execution Context

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:

  1. Provide execution context that persist across async tasks.
  2. Intercept async task, and provide life cycle hooks.
  3. 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

  1. 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.

  1. call testObj.testFunc, this will be testObj.
  2. create a reference newTestFunc, this will be globalThis.
  3. call with apply, this will be newObj.
  4. call bounded version, this will always be bindObj.

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.