4

Change detection in Angular is a mechanism for synchronizing the state of an application's UI with the state of the data. When the application logic changes the component data, the values bound to the DOM properties in the view also change. The change detector is responsible for updating the view to reflect the current data model. Before reading this article, I recommend checking out my first two blog posts that are closely related to change detection, Demystifying Angular Lifecycle Functions and An Introduction to Angular's Zone.js.

On paper, I feel shallow at the end, and I absolutely know that this matter has to be done. In order to make it easier for readers to understand, this article starts with a small example and then expands it step by step. An example is as follows:

// app.component.ts
import { Component } from '@angular/core';
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = 'aa';

  handleClick() {
    this.title = 'bb';
  }
}
// app.componnet.html
<div (click)="handleClick()">{{title}}</div>

The example is relatively simple, which is to bind a click event to the div element. Clicking on the element will change the value of the variable title , and the display of the interface will be updated accordingly. How does the framework know when the view needs to be updated, and how does it update the view? Let's find out.

When we click div element, the handleClick function will be executed. So how is this function triggered and executed in an Angular application? If you've read my previous article on zone.js 's introduction, you'll know that click events in Angular applications have been taken over by zone.js . Based on this answer, it is obvious that it must be triggered by zone.js at the beginning, but here we will further analyze the direct calling relationship and expand it layer by layer. The closest function call to handleClick is the following code:

function wrapListener(listenerFn, ...) {
    return function wrapListenerIn_markDirtyAndPreventDefault(e) {
      let result = executeListenerWithErrorHandling(listenerFn, ...);
    }
}

In the above code, the listenerFn function points to handleClick , but it is also the parameter of the wrapListener function. In the example, the element is bound to a click event, and the relevant template compilation product is probably like this:

function AppComponent_Template(rf, ctx) {
  ......
  i0["ɵɵlistener"]("click", function AppComponent_Template_div_click_0_listener() {
    return ctx.handleClick();
  })
}

When the application is loaded for the first time, executeTemplate renderView be executed, and then the above template function will be triggered, and the click function of the element will be passed all the way to the listenerFn parameter. Here we have learned that the trigger source of the click function is zone.js , but the real click function transfer is implemented by Angular, so how are zone.js and Angular related? zone.js will schedule a task for each asynchronous event. Combined with the example in this article, invokeTask is called by the following code:

function forkInnerZoneWithAngularBehavior(zone) {
  zone._inner = zone._inner.fork({
    name: 'angular',
    properties: { 'isAngularZone': true },
    onInvokeTask: (delegate, current, target, task, applyThis, applyArgs) => {
      try {
        onEnter(zone);
        return delegate.invokeTask(target, task, ...);
      }
      finally {
        onLeave(zone);
      }
    }
  })
}

Is it very familiar to see here, because there are similar code snippets in the previous article introduced by zone.js . The forkInnerZoneWithAngularBehavior function is called by the constructor of the class NgZone. So far, we have introduced NgZone, a protagonist of Angular change detection, which is a simple encapsulation of zone.js .

Now that we know how the click function is executed in the example, after the function is executed, the application data changes, and how does the view update in time? Let's go back to the forkInnerZoneWithAngularBehavior function mentioned above. In the try finally statement block, after executing the invokeTask function, the onLeave(zone) function will eventually be executed. Further analysis, you can see that the onLeave function finally calls the checkStable function:

function checkStable(zone) {
  zone.onMicrotaskEmpty.emit(null);
}

Accordingly subscribe to this emit event in the class ApplicationRef constructor:

class ApplicationRef {
    /** @internal */
    constructor() {
    this._zone.onMicrotaskEmpty.subscribe({
            next: () => {
                this._zone.run(() => {
                    this.tick();
                });
            }
        });
    }

this.tick() familiar in the subscription-related callback functions? If you read my previous article on Angular lifecycle functions, you must have the impression that it is the key call that triggers the view update. Although this function was covered in that life cycle introduction article, the focus of this article is on change detection so the function is the same but with a slightly different focus. The related call sequence of this.tick is probably like this:

this.tick() -> 
view.detectChanges() -> 
renderComponentOrTemplate() ->
refreshView()

Here refreshView is more important to analyze separately:

function refreshView(tView, lView, templateFn, context) {
  ......
  if (templateFn !== null) {
    // 关键代码1
    executeTemplate(tView, lView, templateFn, ...);
  }
  ......
  if (components !== null) {
    // 关键代码2
    refreshChildComponents(lView, components);
  }
}

In this process, the refreshView function will be called twice. The first time it enters the key code 2 branch, and then the following functions are called in turn to re-enter the refreshView function:

refreshChildComponents() -> 
refreshChildComponents() ->
refreshComponent() ->
refreshView()

The second time to enter the refreshView function call is the key code 1 branch, that is, the execution is: executeTemplate function. And this function finally executes the AppComponent_Template function in the template compilation product:

function AppComponent_Template(rf, ctx) {
  if (rf & 1) { // 条件分支1
    i0["ɵɵelementStart"](0, "div", 0);
    i0["ɵɵlistener"]("click", function AppComponent_Template_div_click_0_listener() {
      return ctx.handleClick();
    });
    i0["ɵɵtext"](1);
    i0["ɵɵelementEnd"]();
  } 
  if (rf & 2) { // 条件分支2
    i0["ɵɵadvance"](1);
    i0["ɵɵtextInterpolate"](ctx.title);
  }

If there are still readers who do not know how the functions in the above template compilation products come from, it is recommended to read the previous articles on the principle of dependency injection, which will not be repeated due to space limitations. At this time, the AppComponent_Template function executes the code in the conditional branch 2. The function of the ɵɵadvance function is to update the relevant index value to ensure that the correct element is found. Here we focus on the ɵɵtextInterpolate function, which finally calls the function ɵɵtextInterpolate1 :

function ɵɵtextInterpolate1(prefix, v0, suffix) {
    const lView = getLView();
    // 关键代码1
    const interpolated = interpolation1(lView, prefix, v0, suffix);
    if (interpolated !== NO_CHANGE) {
        // 关键代码2
        textBindingInternal(lView, getSelectedIndex(), interpolated);
    }
    return ɵɵtextInterpolate1;
}

It is worth pointing out that the function name has the number 1 at the end, because there are similar ɵɵtextInterpolate2 , ɵɵtextInterpolate3 , etc., Angular internally calls different special functions according to the number of interpolation expressions, the number of interpolation expressions of the text node in this example is 1, so the ɵɵtextInterpolate1 function is actually called. This function mainly does two things. The key code 1 is to compare whether the value of the interpolation expression is updated, and the key code 2 is to update the value of the text node. Let's first take a look at the function interpolation1 of key code 1, which finally calls:

function bindingUpdated(lView, bindingIndex, value) {
    const oldValue = lView[bindingIndex];
    if (Object.is(oldValue, value)) {
        return false;
    }
    else {
        lView[bindingIndex] = value;
        return true;
    }
}

The text node value before change detection is called oldValue , which is stored in lView . I also mentioned lView in the previous article. Readers who have forgotten it can go and see the role of lView . bindingUpdated will first compare the new value with the old value, the method of comparison is Object.is . Returns false if the old value has not changed from the new value. If there is a change, update the value stored in lView and return true . The function textBindingInternal of key code 2 finally calls the following function:

function updateTextNode(renderer, rNode, value) {
    ngDevMode && ngDevMode.rendererSetText++;
    isProceduralRenderer(renderer) ? renderer.setValue(rNode, value) : rNode.textContent = value;
}

After the above process, when we click on the div element, the interface display content will change from aa to bb , that is, the synchronous update from the application data change to the UI state is completed, which is the most basic change detection process in Angular.

Due to space limitations, the examples in this article are relatively simple, but there is still a lot of Angular change detection that has not been covered. For example, if the application is composed of several components, how to perform change detection between parent and child components, and how to optimize change detection through strategies, etc. If you have friends who are interested in this area, please pay attention to my personal public account [Zhu Yujie's blog], and I will share more front-end knowledge there in the future.


Zuckjet
437 声望657 粉丝

学如逆水行舟,不进则退。