Improving change detection

As we saw earlier, the view in MVC updates itself, based on change events it receives from the model. A number of Model View Whatever (MVW) frameworks took this approach and embedded the observer pattern in the core of their change detection mechanism.

Classical change detection

Let's take a look at a simple example, which doesn't use any framework. Suppose, we have a model called User, which has a property called name:

class User extends EventEmitter { 
  private name: string;
 
  setName(name: string) { 
    this.name = name; 
    this.emit('change');
  }
 
  getName(): string { 
    return this.name;
  } 
} 

The preceding snippet again uses TypeScript. Do not worry if the syntax does not look familiar to you, we will make an introduction to the language in the next chapter.

The user class extends the EventEmitter class. This provides primitives for emitting and subscribing to events.

Now, let's define a view, which displays the name of an instance of the User class, passed as an argument to its constructor:

class View { 
  constructor(user: User, el: Element /* a DOM element */) { 
    el.innerHTML = user.getName();
  } 
} 

We can initialize the view element by:

let user = new User(); 
user.setName('foo'); 
let view = new View(user, document.getElementById('label')); 

As the end result, the user will see a label with the content foo. However, changes in user will not be reflected by the view. In order to update the view when the name of the user changes, we need to subscribe to the change event and then update the content of the DOM element. We need to update the View definition in the following way:

class View { 
  constructor(user:User, el:any /* a DOM element */) { 
    el.innerHTML = user.getName(); 
    user.on('change', () => { 
      el.innerHTML = user.getName();
    }); 
  } 
} 

This is how most frameworks used to implement their change detection before the era of AngularJS.

Change detection in AngularJS

Most beginners are fascinated by the data binding mechanism in AngularJS. The basic "Hello world" example looks similar to this:

function MainCtrl($scope) { 
  $scope.label = 'Hello world!'; 
}
<body ng-app ng-controller="MainCtrl"> 
  {{label}} 
</body> 

If you run this, Hello world! magically appears on the screen. However, that is not the only most impressive thing! If we add a text input and we bind it to the label property of the scope, each change will reflect in the content displayed by the interpolation directive:

<body ng-controller="MainCtrl"> 
  <input ng-model="label"> 
  {{label}} 
</body> 

How awesome is that! This is one of the main selling points of AngularJS-the extreme ease of achieving data binding. We add a few attributes in our markup, interpolation directive, the label property to a mystical object called $scope, which is magically passed to a custom function we define, and everything simply works!

The more experienced Angular developer has a better understanding of what is actually going on behind the scenes. In the preceding example, inside the directives, ng-model and ng-bind (in our case, the interpolation directive, {{}}), Angular adds watchers with different behavior associated with the same expression - label. These watchers are quite similar to the observers in the classical MVC pattern. On some specific events (in our case, change of the content of the text input), AngularJS will loop over all such watchers, evaluate the expressions associated with them in the context of a given scope, and store their results. This loop is known as the digest loop.

In the preceding examples, the evaluation of the expression, label, in the context of the scope will return the text, Hello world!. On each iteration, AngularJS will compare the current result of the evaluation with the previous result and will invoke the associated callback in case the values differ. For instance, the callback added by the interpolation directive will set the content of the element to be the new result of the expression's evaluation. This is an example of the dependency between the callbacks of the watchers of two directives. The callback of the watcher added by ng-model modifies the result of the expression associated with the watcher added by the interpolation directive.

This approach has its own drawbacks. We said that the digest loop will be invoked on some specific events, but what if these events happen outside the framework; for example, what if we use setTimeout, and inside the callback, passed as the first argument, we change properties attached to the scope that we're watching? AngularJS will be unaware of the change and won't invoke the digest loop, so we need to do that explicitly using $scope.$apply. But, what if the framework knew about all the asynchronous events happening in the browser, such as user events, the XMLHttpRequest events, the WebSocket-related events, and others? In such a case, Angular would be able to intercept the event's handling and could invoke the digest loop without forcing us to do so!

In the zone.js

That's exactly the case in the new versions of Angular. This functionality is implemented with zones using zone.js.

At ng-conf in 2014, Brian Ford gave a talk about zones. Brian presented zones as meta-monkey patching of browser APIs. Zone.js is a library developed by the Angular team, which implements zones in JavaScript. They represent an execution context, which allows us to intercept asynchronous browser calls. Basically, using zones, we are able to invoke a piece of logic just after the given XMLHttpRequest completes or when we receive a new WebSocket event. Angular took advantage of zone.js by intercepting asynchronous browser events and invoking the digest loop just at the right time. This totally eliminates the need for explicit calls of the digest loop by the developer using Angular.

Simplified data flow

The cross-watcher dependencies may create a tangled data flow in our application, which is hard to follow. This may lead to unpredictable behavior and bugs, which are hard to find. Although Angular kept the dirty checking as a way to achieve change detection, it enforced unidirectional data flow. This happened by disallowing dependencies between the different watchers, which allows the digest loop to be run only once. This strategy increases the performance of our applications dramatically and reduces the complexity of the data flow. Angular also made improvements to memory efficiency and the performance of the digest loop. Further details on Angular's change detection and the different strategies used for its implementation can be found in Chapter 4, Getting Started with Angular Components and Directives.

Enhancing AngularJS's change detection

Now, let's take a step back and again think about the change detection mechanism of the framework.

We said that inside the digest loop, Angular evaluates registered expressions and compares the evaluated values with the values associated with the same expressions in the previous iteration of the loop.

The most optimal algorithm used for the comparison may differ depending on the type of the value returned from the expression's evaluation. For instance, if we get a mutable list of items, we need to loop over the entire collection and compare the items in the collections one by one in order to verify if there is a change or not. However, if we have an immutable list, we can perform a check with a constant complexity, only by comparing references. This is the case because the instances of immutable data structures cannot change. Instead of applying an operation, which intends to modify such instances, we'll get a new reference with the modification applied.

In AngularJS, we can add watchers using a few methods. Two of them are $watch(exp, fn, deep) and $watchCollection(exp, fn). These methods give us some level of control over the way the change detection will perform the equality check. For example, adding a watcher using $watch and passing a false value as a third argument will make AngularJS perform a reference check (that is, compare the current value with the previous one using ===). However, if we pass a truthy (any true value), the check will be deep (that is, using angular.equals). This way, depending on the expected type of the return by the expression value, we can add listeners in the most appropriate way in order to allow the framework to perform equality checks with the most optimal algorithm available. This API has two limitations:

  • It does not allow you to choose the most appropriate equality check algorithm at runtime.
  • It does not allow you to extend the change detection to third parties for their specific data structures.

The Angular core team assigned this responsibility to differs, allowing them to extend the change detection mechanism and optimize it, based on the data we use in our applications. Angular defines two base classes, which we can extend in order to define custom algorithms:

  • KeyValueDiffer: This allows us to perform advanced diffing over key value-based data structures.
  • IterableDiffer: This allows us to perform advanced diffing over list-like data structures.

Angular allows us to take full control over the change detection mechanism by extending it with custom algorithms, which wasn't possible in the previous version of the framework. We'll take a further look into the change detection and how we can configure it in Chapter 4, Getting Started with Angular Components and Directives.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset