preface

When I first encountered Angular, I was impressed by its framework design and worried about its performance. Why am I worried about its performance? Because I noticed that Angular updates the DOM automatically, in my mind there should be a timed loop that triggers detection over and over again, and synchronizes data into the DOM if it changes. As it turns out, angular internal data updates don’t always trigger, but rather need to meet a certain condition. The term for this automatic change mechanism is change detection.

Zone

Before introducing the principle of change detection, let’s introduce an excellent JS library called Zone. I have reviewed a lot of information about Zone, and I will mainly explain my understanding of it here. A Zone, as its name implies, is also an execution context in which asynchronous or synchronous code is executed within the same Zone. A Zone can hold variables that are shared by the context within the same Zone, and a Zone provides hooks for monitoring the status of tasks (microtasks, macro tasks) within the Zone. The use of a Zone is very similar to multithreading. When a thread is started, it has a main thread. From the main thread, one or more sub-threads can be separated, and the sub-threads can also continue the sub-threads. The same is true for zones. A Zone starts with a Root Zone, which can be divided into sub-zones.

Zone Thread
The root environment Root Zone Main Thread
Can be divided Child Zone Child Thread
Variables store Zone Spec Thread Local

I have seen that Zone is an environment in which variables can be stored and used by all tasks in the environment. In addition, there are many hooks in the environment, which can detect the status of tasks. If you want different tasks to use different environments, how do you separate them? You only need to split the current Zone. For variables, child zones can inherit from parent zones, and sibling zones are separated from each other.

It is worth noting that whenever they are in a Zone environment, whether asynchronous or synchronous, they share the context of the current Zone. What is the significance of this? As we all know, Js is single threaded (here refers to the Js engine thread, interested can refer to the Js thread model related information), so there is no multi-threading, so asynchronous programming in Js, Whether SetTimeout and SetInterval, Promise, observables are based on the Event Loop, namely a message queue, all the Task into the queue first, and then to the rules of fifo, perform the Task. This creates a sense of asynchrony. Based on this queue model, it is difficult to establish communication between tasks. You also have no idea how well these tasks are performing.

What can a Zone be used for

JavaScript virtual machines embedded in the host environment, such as a browser or Node, are responsible for scheduling JavaScript execution through tasks. A Zone is an execution context that persists across asynchronous tasks and allows the creator of the Zone to observe and control the execution of code within that Zone. Angular’s change detection is zone-based, but that doesn’t mean that a Zone can only be used for change detection. A Zone can also do a lot of things:

  1. Zones are useful for debugging, testing, and analysis.
  2. Zones are useful for the framework to know when to render.
  3. Zones are useful for tracking resources that are persisted across asynchronous operations and can be freed/cleansed automatically.
  4. Zones are composable

These are excerpts from official documents. From the documentation, we can see that the Zone is mainly for the execution context of asynchronous tasks.

How to Use Zone

You should have a general idea of how to use this powerful Zone library. And how do you manage persistent resources asynchronously?

  • Gets the Zone reference and creates a subzone
// Get the current Root Zone,
let rootZone = Zone.current;
// Diverts sub-zones based on the current Root Zone.
zoneA = rootZone.fork({name: 'zoneA'}); \// The default name of the Root Zone is < Root >
expect(rootZone.name).toEqual('<root>');
// Get the name of the subzone
expect(zoneA.name).toEqual('zoneA');
// Get a reference to the parent Zone via the parent reference
expect(zoneA.parent).toBe(rootZone);
Copy the code
  • Asynchronous tasks are executed in the Root Zone
let rootZone = Zone.current; let zoneA = rootZone.fork({name: 'A'}); expect(Zone.current).toBe(rootZone); // If the Zone is not configured, By default, setTimeout(function timeoutCb1() {// This callback expects (zone.current).toequal (rootZone) in the Root Zone; }, 0);Copy the code
  • Asynchronous tasks execute in the specified subzone — zonea.run ()
Run (function run1() {expect(zone.current).toequal (zoneA); // Execute zonea.run (function run1() {expect(zone.current).toequal (zoneA); SetTimeout (function timeoutCb2() {// Expect (zone.current).toequal (ZoneA); }, 0); });Copy the code

Based on the introduction to zones above, we learned what a Zone is, how to create a Zone, and how to execute code in a given Zone.

Task classification

JS has the following Task categories: Main Task, Micro Task, Macro Task, Event Task.

Main Task: is a common block of code, such as assignment, four operations. These operations are typically executed immediately, unlike other tasks that are first stuffed into the Event loop queue.

Micro Task: Mainly refers to the promise, in the event loop queue, is characterized by execution as fast as possible, and can only be executed once without cancellation.

Macro Task: setTimeout,setInterval. Macro Task is executed after Micro Task in the Event loop queue.

Event Task: Such as Click, Mouse move/over/ Enter, etc. An Event Task is characterized by the fact that the execution of the Task may never occur, may occur at unpredictable times, and may occur more than once, so there is no way to know how many times it will be executed. Task is much more than that, and here is a brief introduction.

Why are there so many different tasks? Once you understand the sequence of tasks, you will know when the DOM will be rendered and when the DOM values have been updated. If the dom rendering code and the HTTP request code are both in the Main Task, that is, executed sequentially, the HTTP request will block the JS thread from executing the following render code. Much of the code that needs to be rendered does not require the HTTP request value. That is, it should be rendered in advance. Put the HTTP request in your Macro Task, and when all the I/O or page rendering is done, make the HTTP request, and then update the DOM after you get the data, so when you open a web page, you’ll see the page first, and then the page values will be updated. This feels like an asynchronous operation, which is the right interaction logic, and the user can’t wait.

In this example, we learned that data changes should be put into Macro Task, which means that whenever a Macro Task happens, I think I need to update the page data. So did you Get it? This is how Angular automatically updates pages and performs change detection. The question is, how does Angular know which tasks are executed, which are executing, and who notifies Angular that change detection should be done? The answer is Zone.

NgZone

A zone is a persistent execution context across asynchronous tasks, which you can think of as thread local storage for the JS VM. Here we show how NgZone automatically performs change detection to update HTML pages.

Display and update data in Angular

In Angular you can bind properties to HTML pages, like title or myHero.

import { Component } from '@angular/core';

@Component({
  selector: 'app-root'.template: ` 

{{title}}

My favorite hero is: {{myHero}}

`
}) export class AppComponent { title = 'Tour of Heroes'; myHero = 'Windstorm'; } Copy the code

You can also bind an Angular component to a Click event that updates the component properties

@Component({
    selector: 'app-click-me'.template: `  {{clickMessage}}`
  })
  export class ClickMeComponent {
    clickMessage = ' ';
  
    onClickMe() {
      this.clickMessage = 'You are my hero! '; }}Copy the code

In the two examples above, the component attributes are updated, but the HTML is automatically updated. Here’s how and when Angular renders Angular HTML pages based on attributes.

Use pure javascript to detect changes

To illustrate how to detect updated data, let’s just use javascript to describe it

<html>
  <div id="dataDiv"></div>
  <button id="btn">updateData</button>
  <canvas id="canvas"></canvas>
  <script>
    let value = 'initialValue';
    // Initialize render
    detectChange();

    function renderHTML() {
      document.getElementById('dataDiv').innerText = value;
    }

    function detectChange() {
      const currentValue = document.getElementById('dataDiv').innerText;
      if (currentValue !== value) {
        renderHTML();
      }
    }

    // Example 1: Update data via click button
    document.getElementById('btn').addEventListener('click'.() = > {
      // Update data
      value = 'button update value';
      DetectChange manually
      detectChange();
    });

    // Example 2: HTTP Request
    const xhr = new XMLHttpRequest();
    xhr.addEventListener('load'.function() {
      // Get the response data from sever
      value = this.responseText;
      DetectChange manually
      detectChange();
    });
    xhr.open('GET', serverUrl);
    xhr.send();

    // Example 3: The setTimeout macro task updates data
    setTimeout(() = > {
      // setTimeout calls back to update data
      value = 'timeout update value';
      DetectChange manually
      detectChange();
    }, 100);

    // Example 4: Promise
    Promise.resolve('promise resolved a value').then(v= > {
      // Promise thenCallback updates data internally
      value = v;
      DetectChange manually
      detectChange();
    }, 100);

    // Example 5: Other asynchronous apis update data
    document.getElementById('canvas').toBlob(blob= > {
      Update blob data when it is created from the canvas
      value = `value updated by canvas, size is ${blob.size}`;
      DetectChange manually
      detectChange();
    });
  </script>
</html>
Copy the code

Judging from the code above, when the data is updated, you need to manually call detectChange to check for changes and re-render the DOM if they happen. In Angular, these manual calls to check for updates are avoided; the DOM is automatically updated whenever data is updated.

When should I update my HTML

To understand how change detection works, the first thing to think about is when these pages should be updated. Updates generally occur in one of the following ways:

  • Component initialization

For example, when an Angular app is launched, Angular loads the bootstrap component, calls change detection by calling ApplicationRef.tick(), and renders the view.

  • Listen for an event

Dom events can update data in Angular components and trigger change detection

@Component({
    selector: 'app-click-me'.template: `  {{clickMessage}}`
  })
  export class ClickMeComponent {
    clickMessage = ' ';
  
    onClickMe() {
      this.clickMessage = 'You are my hero! '; }}Copy the code
  • HTTP requests, you can get data from the server
@Component({
    selector: 'app-root'.template: '<div>{{data}}</div>';
  })
  export class AppComponent implements OnInit {
    data = 'initial value';
    serverUrl = 'SERVER_URL';
    constructor(private httpClient: HttpClient) {}
  
    ngOnInit() {
      this.httpClient.get(this.serverUrl).subscribe(response= > {
        // user does not need to trigger change detection manually
        this.data = response.data; }); }}Copy the code
  • MacroTasks, such assetTimeout() or setInterval()
@Component({
    selector: 'app-root'.template: '<div>{{data}}</div>';
  })
  export class AppComponent implements OnInit {
    data = 'initial value';
  
    ngOnInit() {
      setTimeout(() = > {
        // user does not need to trigger change detection manually
        this.data = 'value updated'; }); }}Copy the code
  • MicroTasks,Promise.thenOr other asynchronous methods return a Promise object and update the data by calling THEN
@Component({
    selector: 'app-root'.template: '<div>{{data}}</div>';
  })
  export class AppComponent implements OnInit {
    data = 'initial value';
  
    ngOnInit() {
      Promise.resolve(1).then(v= > {
        // There is no need for users to manually invoke change detection
        this.data = v; }); }}Copy the code
  • Other asynchronous operations, in addition to addEventListener(), setTimeout() and promise.then (), there are other operations that can be updated automatically, such as:WebSocket.onmessage() and Canvas.toBlob()

The previous examples were generic scenarios where Angular might update data, so Angular does change detection whenever there is a possibility of updating data. Anuglar triggers change detection in different ways. Angular triggers change detection directly when a component is initialized, and angualr uses zones to trigger change detection where data changes are likely to occur for asynchronous operations.

Zones and execution context

A zone provides a persistent execution context across asynchronous tasks. An execution context is an abstract concept that holds information about the environment within the currently executing code. Here’s an example:

const callback = function() {
  console.log('setTimeout callback context is'.this);
}

const ctx1 = { name: 'ctx1' };
const ctx2 = { name: 'ctx2' };

const func = function() {
  console.log('caller context is'.this);
  setTimeout(callback);
}

func.apply(ctx1);
func.apply(ctx2);
Copy the code

The value of this in the callback function setTimeout() may be different, depending on when setTimeout() is called, so you may lose context information in asynchronous tasks. The apply method in JS is a code that can change the context. The context is changed to CTx1 and CTx2 respectively, and it is not difficult to find this context and ctX2. It is not difficult to find the connection between these two asynchronous setTimeout tasks.

The zone provides a new context, independent of this, that is persistent across the asynchronous task. In the following example, we will call this context zoneThis. It is not hard to see that the same zone context exists outside and inside the asynchronous task.

zone.run(() = > {
  // Now you are in a zone
  expect(zoneThis).toBe(zone);
  setTimeout(function() {
    // zoneThis has the same content as the zone
    // When setTimeout is called
    expect(zoneThis).toBe(zone);
  });
});
Copy the code

Zones and asynchronous life cycle hooks

Zones not only provide a persistent context across asynchronous tasks, but also provide associated asynchronous task lifecycle hooks

const zone = Zone.current.fork({
  name: 'zone'.onScheduleTask: function(delegate, curr, target, task) {
    console.log('new task is scheduled:', task.type, task.source);
    return delegate.scheduleTask(target, task);
  },
  onInvokeTask: function(delegate, curr, target, task, applyThis, applyArgs) {
    console.log('task will be invoked:', task.type, task.source);
    return delegate.invokeTask(target, task, applyThis, applyArgs);
  },
  onHasTask: function(delegate, curr, target, hasTaskState) {
    console.log('task state changed in the zone:', hasTaskState);
    return delegate.hasTask(target, hasTaskState);
  },
  onInvoke: function(delegate, curr, target, callback, applyThis, applyArgs) {
    console.log('the callback will be invoked:', callback);
    returndelegate.invoke(target, callback, applyThis, applyArgs); }}); zone.run(() = > {
  setTimeout(() = > {
    console.log('timeout callback is invoked.');
  });
});
Copy the code

The example above not only creates a sub-zone, but also defines a hook for an asynchronous task that will be triggered when the status of the asynchronous task changes.

The concept of a Zone task is similar to that of a Javascript VM task.

Javascript VM tasks:

  • macroTask: such as setTimeout()
  • microTask: such as Promise.then()
  • eventTask: such as element.addEventListener()

A hook is triggered in one of the following situations:

  • onScheduleTask: When asynchronous tasks are scheduled, such as when you callsetTimeout()“, the hook will be triggered.
  • onInvokeTask: When an asynchronous task is executed, such as whensetTimeout()The inside of thecallbackExecution time.
  • onHasTask: This function is invoked when the status of a task changes from stable to unstable or from unstable to stable in the Zone. Stable means that there are no tasks in the Zone. Unstable means that a new task is scheduled in the Zone.
  • onInvoke: called when the synchronized method is about to be executed in the Zone.

With these hooks, you can monitor the status of synchronous and asynchronous tasks in the Zone. The above example will output the following:

When zone.run is executed, the following output is displayed:

// Synchronize content in ngzone
the callback will be invoked: () = > {
  setTimeout(() = > {
    console.log('timeout callback is invoked.');
  });
}
// When setTimeout is executed, the callback function is not called but placed in the task queue, calling onScheduleTask
new task is scheduled: macroTask setTimeout
// From unstable state to stable state, call onHasTask
task state changed in the zone: { microTask: false.macroTask: true.eventTask: false.change: 'macroTask' }
// The callback function in setTimeout is called, calling onInvokeTask
task will be invoked macroTask: setTimeout
//console.log prints out the content
timeout callback is invoked.
// The asynchronous task is transitioned from stable to unstable
task state changed in the zone: { microTask: false.macroTask: false.eventTask: false.change: 'macroTask' }
Copy the code

All of the methods in the Zone are provided by the zone.js library, which implements these features through the monkey patch intercepting asynchronous apis. Monkey patches modify or add the default behavior of a method at run time without changing the source code.

NgZone

Angular provides the NgZone service, although zone.js can monitor the status of all asynchronous or synchronous operations. This service creates a zone named Angular that automatically triggers change detection when:

  • An asynchronous or synchronous task is executed
  • When there is nomicroTaskWhen they were deployed

NgZone run() and runOutsideOfAngular()

A Zone can handle most asynchronous tasks, such as setTimeout(), promise.then (), and addEventListener(), and can handle all asynchronous task list documents. So with these asynchronous apis, there is no need to manually invoke change detection. However, sometimes you may call third-party library apis that are not handled by the Zone and therefore do not trigger change detection. The NgZone Service provides a run() method that runs the callback method you execute in the Angular Zone. This automatically triggers change detection when a third-party API function is called. Here’s an example: The ResizeObserver function is one you may not have heard of, but you may have encountered the need to monitor window.onresize. Monitoring window.size is inconvenient on the one hand and inefficient on the other. ResizeObserver addresses this problem. So when I use this function in my project, the page data does not refresh, and the introduction of NgZone is a perfect solution.

export class AppComponent implements OnInit {
  constructor(private ngZone: NgZone) {}
  ngOnInit() {
    // The new asynchronous API will not be handled by the Zone, so you need to use ngzone.run () to execute the asynchronous task in the Angular Zone.
    // Automatically triggers change detection
    this.ngZone.run(() = > {
      someNewAsyncAPI(() = > {
        // Update component data}); }); }}Copy the code

By default, all asynchronous tasks are executed in the Angular zone, but there are scenarios where you don’t want to trigger change checks while these asynchronous tasks are executing. In this case, another method is available: runOutsideAngular().

export class AppComponent implements OnInit {
  constructor(private ngZone: NgZone) {}
  ngOnInit() {
    // You are sure that no data changes will occur
    // Therefore you do not want to trigger change detection after the execution of the method is complete
    / / call ngZone. RunOutsideAngular ()
    this.ngZone.runOutsideAngular(() = > {
      setTimeout(() = > {
        // Update component data without triggering change detection}); }); }}Copy the code

conclusion

This article first gives a detailed introduction to zone.js, and then introduces how to implement change detection in pure Javascript. This part is intended to give readers an idea of when to update the DOM based on updated component data, and then introduces NgZone, which makes it unnecessary to manually invoke change detection. The introduction of NgZone in this article is mainly a translation of the official document. The document is too clear and lacks Chinese. There are many knowledge points about zones and NgZone, if you want to know more, you can refer to the following reference links.

reference

Developer.mozilla.org/zh-CN/docs/…

angular.io/guide/zone

zhuanlan.zhihu.com/p/50835920

Blog. Presents – university. IO/how does – an…