Js’ $digest is reborn in the newer version of Angular

I’ve been using the Angular.js framework for a few years now, and despite the criticism it’s received, I still think it’s an incredible framework. I started learning from Building Your Own Angular.js and read a lot of source code for the framework, so I feel familiar with the internal mechanics of Angular.js and the architectural ideas that created the framework. Recently I’ve been trying to master the internal architecture of the new Angular framework and compare it to the old angular.js internal architecture. I found out that Angular borrowed a lot from angular.js.

One of them is the infamous Digest Loop:

The main problem with the design is that it costs too much. Changing anything in a program requires executing hundreds of functions to find out which data has changed. This is a fundamental part of Angular, but it limits queries to a subset of the UI to improve performance.

If you can better understand how Angular implements digest, you can design your application more efficiently, for example, using $scope.$digest() instead of $scope.$apply, or using immutable objects. But the truth is, it may not be easy for many people to design more efficient programs to understand the implementation within the framework.

So a lot of Angular articles and tutorials claim that there is no more $Digest Cycle in the framework. It depends on what you think of the digest concept, but I think it’s misleading because it still exists. True, there are no scopes and Watchers in Angular, and you no longer need to call $scope.$digest(), but the mechanism for detecting data changes is still to traverse the component tree, implicitly invoke Watchers, and update the DOM. So it’s actually completely rewritten, but optimized and enhanced. For the new query mechanism, see my Everything You Need to Know about change Detection in Angular.

The need for digest

Before we begin, let’s recall why digest exists in Angular.js. All frameworks solve the synchronization problem of data model (JavaScript Objects) and UI (Browser DOM). The biggest problem is how to know when the data model changes, and the process of querying when the data model changes is change detection. The different implementations of this problem are one of the biggest differentiators of today’s front-end frameworks. I plan to write an article about how different frameworks compare change detection implementations, so if you’re interested and want to be notified, follow me.

There are two ways to detect changes: you need a consumer notification framework; Automatically detect changes by comparison.

Suppose we have the following object:

let person = {name: 'Angular'};
Copy the code

We then update the name property value, but how does the framework know when the value is updated? One way is to require the user to tell the framework:

constructor() {
    let person = {name: 'Angular'};
    this.state = person; }...// explicitly notifying React about the changes
// and specifying what is about to change
this.setState({name: 'Changed'});
Copy the code

Or force the user to encapsulate the property so that the framework can add setters:

let app = new Vue({
    data: {
        name: 'Hello Vue! '}});// the setter is triggered so Vue knows what changed
app.name = 'Changed';
Copy the code

Another way is to save the last value of the name attribute and compare it to the current value:

if(previousValue ! == person.name)// change detected, update DOM
Copy the code

But when does the comparison end? We should check each time asynchronous code is run, since this part of the running code is handled as an asynchronous event, known as a Virtual Machine(VM) turn/tick. The data change check code can be executed immediately after the VM turn. This is why Angular.js uses digest, so we can define digest as:

a change detection mechanism that walks the tree of components, Checks each component for changes and updates DOM when a component property is changed.

If we define digest this way, THEN I can say that the main part of the data-change checking mechanism doesn’t change in Angular, but the implementation of Digest does.

Angular.js

Angular.js uses the concept of watcher and listener. Watcher is a function that returns a monitored value, which most of the time is a property of the data model. But not always data model properties, for example we can track component state in scope, calculate property values, third-party components, and so on. If the current return value is different from the previous one, angular.js calls the Listener, which is usually used to update the UI.

The argument list for the $watch function is as follows:

$watch(watcher, listener);
Copy the code

So, if we had a person object with a name attribute and used {{name}} in the template, we could update the DOM by tracking the attribute change like this:

$watch((a)= > {
    return person.name
}, (value) = > {
    span.textContent = value
});
Copy the code

Angular.js uses directives to map DOM’s data model in the same way that interpolation and ng-bind directives essentially do. Angular doesn’t do that anymore. It uses property mapping to connect the data model to the DOM. The above example is implemented in Angular like this:

<span [textContent] ="person.name"></span>
Copy the code

Because there are many components and they form a component tree, and each component has a different data model, there is a hierarchical Watchers, which is similar to the hierarchical component tree. Although scopes are used to group Watchers together, they are not related.

Angular.js now traverses the Watchers tree and updates the DOM during digest. If you use $timeout, $HTTP, or $scope.$apply and $scope.$digest as needed, the Digest Cycle will be triggered on each asynchronous event.

Watchers is triggered in strict sequence: first the parent component, then the child component. This makes sense, but has an unwelcome downside. A triggered Watcher Listener has many side effects, including updating the parent component’s properties. If the parent listener is already triggered and the child listener updates the parent component property, the change will not be detected. That’s why the Digest Loop runs multiple times to get a stable state of the program, ensuring that no data changes. The maximum number of runs is 10. This design is now considered flawed, and Angular does not allow it.

Angular

Angular doesn’t have the watcher concept in Angular.js, but there are still functions to track model properties. These functions are generated by the framework compiler and are private and not accessible. In addition, they are tightly coupled to the DOM, and these functions are stored in the updateRenderer that generates the view structure ViewDefinition.

They are also special: only model changes are tracked, not all data changes like angular.js. Each component has a Watcher to track component properties used in the template and calls the checkAndUpdateTextInline function for each monitored property. This function compares the last value of the property to the current value and updates the DOM if it changes.

For example, the template for the AppComponent:

<h1>Hello {{model.name}}</h1>
Copy the code

Angular Compiler generates code like this:

function View_AppComponent_0(l) {
    // jit_viewDef2 is `viewDef` constructor
    return jit_viewDef2(0.// array of nodes generated from the template
        // first node for `h1` element
        // second node is textNode for `Hello {{model.name}}`[ jit_elementDef3(...), jit_textDef4(...) ] .// updateRenderer function similar to a watcher
        function (ck, v) {
            var co = v.component;
            // gets current value for the component `name` property
            var currVal_0 = co.model.name;
            // calls CheckAndUpdateNode function passing
            // currentView and node index (1) which uses
            // interpolated `currVal_0` value
            ck(v, 1.0, currVal_0);
        });
}
Copy the code

Note: Use angular-cli ng new for a new project, run ng serve, The **.ngfactory.js file generated after compiling the component can be found in the ng:// field of the Source Tab of Chrome Dev Tools.

So, even if watcher implements it differently, the Digest loop still exists, just by changing its name to Change Detection Cycle:

In development mode, tick() also performs a second change detection cycle to ensure that no further changes are detected.

Angular.js iterates through the Watchers tree and updates the DOM during digest, which is very similar to the Angular mechanism. Angular also iterates through the component tree and calls a render function to update the DOM during the change-detection cycle. This procedure is part of the checking and updating view process and I also wrote a long article about Everything you need to know about change detection in Angular.

Just like angular. js, change detection in Angular is triggered by asynchronous events. User clicks button event; SetTimeout/setInterval). But because Angular uses the zone package to patch all asynchronous events, there is no need to manually trigger change detection for most asynchronous events. The Angular framework subscribs to the onMicrotaskEmpty event and notifies the Angular framework when an asynchronous event completes, The onMicrotaskEmpty event is fired when no task exists in the microTasks queue of the current VM Turn. However, change detection can also be triggered manually, such as with View.DetectChanges or ApplicationRef.tick (Note: DetectChanges triggers change detection for the current component and its children, and ApplicationRef.tick triggers change detection for the entire component tree.

Angular emphasizes the so-called one-way data flow from the top to the bottom. Components in the lower hierarchy, known as children, are not allowed to change the properties of the parent component after the parent component completes change detection. However, if a component changes the parent property in the DoCheck lifecycle hook, it is ok because the hook function is called before the parent property changes are updated. Step 6, DoCheck, Call at step 9 updates DOM interpolations for the current view if properties on current view component instance changed. However, if the parent property is changed in another phase, such as the AfterViewChecked hook function, and the parent is called after the change has been detected, the framework will throw an error in developer mode:

Expression has changed after it was checked

About this error, you can read this article Everything you need to know about the ExpressionChangedAfterItHasBeenCheckedError error. (Note: This article has been translated)

Angular does not throw errors in production, but does not check for data changes until the next change-detection cycle. (Note: Because Angular executes a change-detection loop twice in developer mode, the second check will throw an error if the parent component property is changed, and only once in production.)

Use lifecycle hooks to track data changes

In Angular.js, each component defines a bunch of watchers to track the following data changes:

  • Properties bound by the parent component
  • Properties of the current component
  • Calculate attribute values
  • A third-party component outside the angular. js system

Angular uses the OnChanges lifecycle hook function to listen for parent properties. You can use the DoCheck lifecycle hook to listen for the current component property. Because the hook function is called before Angular processes the current component property change, you can do whatever you need in this function to get the value of the change to be displayed in the UI. You can also use the OnInit hook function to listen for third-party components and run the change-detection loop manually.

For example, we have a component that displays the current Time, which is provided by the Time service, implemented in angular.js like this:

function link(scope, element) {
    scope.$watch((a)= > {
        return Time.getCurrentTime();
    }, (value) = >{ $scope.time = value; })}Copy the code

Angular implements it this way:

class TimeComponent {
    ngDoCheck()
    {
        this.time = Time.getCurrentTime(); }}Copy the code

Another example is if we have a third-party slider component that is not integrated into the Angular system, but we need to display the current Slide, we simply wrap this component inside the Angular component and listen for the Slider’s Changed event. Manually trigger the change detection loop to synchronize the UI. Angular.js:

function link(scope, element) {
    slider.on('changed', (slide) => {
        scope.slide = slide;
        
        // detect changes on the current component
        $scope.$digest(a); // or run change detectionfor the all app
        $rootScope.$digest();
    })
}
Copy the code

This works the same way in Angular (note: you also need to manually trigger the change-detection loop, this.appref.tick () checks all components, and this.cd.detectChanges() checks the current component and its children) :

class SliderComponent {
    ngOnInit() {
        slider.on('changed'.(slide) = > {
            this.slide = slide

            // detect changes on the current component
            // this.cd is an injected ChangeDetector instance
            this.cd.detectChanges();

            // or run change detection for the all app
            // this.appRef is an ApplicationRef instance
            this.appRef.tick(); }}})Copy the code