The mechanics of DOM Updates in Angular

DOM updates triggered by model changes are an important feature of all front-end frameworks, and Angular is no exception. Define a template expression as follows:

<span>Hello {{name}}</span>
Copy the code

Or property binding similar to the following (note: this is equivalent to the code above) :

<span [textContent] ="'Hello ' + name"></span>
Copy the code

Angular magically updates the DOM element automatically every time the name value changes. This looks simple on the surface, but inside it’s quite complicated. Furthermore, DOM updates are only part of Angular’s change-detection mechanism, which consists of three steps:

  • DOM Updates (note: What this article will explain)
  • child components Input bindings updates
  • query list updates

This article explores the render part of the change detection mechanism (DOM Updates). If you’ve been curious about this question before, read on and it will definitely enlighten you.

When referencing the source code, assume that the program is running in production mode. Let’s get started!

Program internal architecture

Before exploring DOM updates, let’s take a quick look at how Angular applications are designed internally.

view

From my article Here is what You need to know about Dynamic Components in Angular you know that the Angular compiler compiles components used in an application into a factory class. For example, the following code shows how Angular creates a component from a factory class (note: the Angular compiler compiles a factory class, but the compiler does it for you. The following code shows how a developer can manually create a Component instance with ComponentFactory. Anyway, he wants to say how the component is instantiated) :

const factory = r.resolveComponentFactory(AComponent);
componentRef: ComponentRef<AComponent> = factory.create(injector);
Copy the code

Angular uses this factory class to instantiate a View Definition, and then uses the viewDef function to create the View. Angular internally views an application as a view tree. Although an application has many components, it has a common view definition interface that defines the view structure generated by the component. ViewDefinition Interface), of course Angular uses each component object to create a corresponding view, which makes up a view tree. (Note: The main concept here is a view, which is structured as a ViewDefinition Interface.)

Component factory

ComponentFactory most of the code is made up of different view nodes generated by the compiler through template parsing. (note: a compiler-generated ComponentFactory is a function that returns a function value. Of course, they both point to the same thing, just in different forms). Suppose we define a template for a component as follows:

<span>I am {{name}}</span>
Copy the code

The compiler parses this template to generate component factory code that contains something similar to the following (note: this is only the most important part of the code) :

function View_AComponent_0(l) {
    return jit_viewDef1(0,
        [
          jit_elementDef2(0.null.null.1.'span',...). , jit_textDef3(null['I am '. ] )].null.function(_ck,_v) {
            var _co = _v.component;
            var currVal_0 = _co.name;
            _ck(_v,1.0,currVal_0);
Copy the code

Note: The complete code for the factory function compiled by the AppComponent is shown below

 (function(jit_createRendererType2_0,jit_viewDef_1,jit_elementDef_2,jit_textDef_3) {
     var styles_AppComponent = [' '];
     var RenderType_AppComponent = jit_createRendererType2_0({encapsulation:0,styles:styles_AppComponent,data:{}});
     function View_AppComponent_0(_l) {
         returnJit_viewDef_1 (0, [(_l () (), jit_elementDef_2 (0, 0, null, null, 1,'span',[],null,null,null,null,null)),
                (_l()(),jit_textDef_3(1,null,['I am '.' ']))
            ],
            null,
            function(_ck,_v) { var _co = _v.component; var currVal_0 = _co.name; _ck (_v, 1, 0, currVal_0); }); }return{RenderType_AppComponent:RenderType_AppComponent,View_AppComponent_0:View_AppComponent_0}; })Copy the code

The code above describes the structure of the view and is called when the component is instantiated. The jit_viewDef_1 function is actually the viewDef function, which is used to create a view.

The second argument to the viewDef function, Nodes, means something like nodes in HTML, but more than that. The second argument in the above code is an array whose first array element, jit_elementDef_2, is the element node definition and the second array element, jit_textDef_3, is the text node definition. The Angular compiler generates many different node definitions, and node types are set by NodeFlags. We’ll see how Angular updates DOM based on node types later.

This article is only interested in elements and text nodes:

export const enum NodeFlags {
    TypeElement = 1 << 0, 
    TypeText = 1 << 1
Copy the code

Let’s do it briefly.

Note: The core of what the author said above is that the program is made up of a bunch of views, and each view is made up of different types of nodes. This article is only concerned with element nodes and text nodes, and there is an important instruction node in another article.

Structure definition of element node

The element node structure is the node structure that Angular generates for each HTML element. It is also used to generate components. See Here is Why You will not find Components inside Angular for more information. Element nodes can also contain other element nodes and text nodes as children, and the number of children is set by childCount.

All element definitions are generated by the elementRef function, which is jit_elementDef_2() in the factory function. ElementRef () takes the following general parameters:

Name Description
childCount specifies how many children the current element have
namespaceAndName The name of the HTML element
fixedAttrs attributes defined on the element

There are several other parameters with specific properties:

Name Description
matchedQueriesDsl used when querying child nodes
ngContentIndex used for node projection
bindings used for dom and bound properties update
outputs, handleEvent used for event propagation

This article is mainly interested in Bindings.

Note: A view is composed of nodes of different types. Element nodes are generated by the elementRef function. The structure of element nodes is defined by ElementDef.

Structure definition of a text node

The text node structure is the node structure that Angular generates for every HTML text compiled. It is usually a child of an element-definition node, as in our example in this article (note: I am {{name}}
, where SPAN is an element node and I am {{name}} is a text node, which is also a child of SPAN). This text node is generated by the textDef function. Its second argument is passed in as an array of strings (note: Angular V5.* is the third argument). For example, the following text:

<h1>Hello {{name}} and another {{prop}}</h1>
Copy the code

To be parsed as an array:

["Hello "." and another ".""]
Copy the code

It is then used to generate the correct binding:

{
  text: 'Hello',
  bindings: [
    {
      name: 'name',
      suffix: ' and another '
    },
    {
      name: 'prop',
      suffix: ' '}}]Copy the code

This is used to generate text during the dirty check phase:

text
+ context[bindings[0][property]] + context[bindings[0][suffix]]
+ context[bindings[1][property]] + context[bindings[1][suffix]]
Copy the code

Note: As above, the text node is generated by the textDef function, and the structure is defined by textDef. Now that we know how to define and generate two nodes, how does Angular handle binding properties on nodes?

Binding of nodes

Angular uses BindingDef to define binding dependencies for each node. These binding dependencies are usually properties of component classes. Angular uses these bindings to determine how to update nodes and provide context information during change detection. Which operation is determined by BindingFlags. The following list shows the specific DOM operation types:

Name Construction in template
TypeElementAttribute attr.name
TypeElementClass class.name
TypeElementStyle style.name

Element and text definitions create these binding dependencies internally based on binding flag bits that are recognized by the compiler. Each node type has a different binding generation logic. Angular generates the corresponding BindingDef for BindingFlags.

Update renderer

The last parameter in jit_viewDef_1 is of most interest to us:

function(_ck,_v) { var _co = _v.component; var currVal_0 = _co.name; _ck (_v, 1, 0, currVal_0); });Copy the code

This function is called updateRenderer. It takes two arguments: _ck and _v. _ck is short for check, which is the prodCheckAndUpdateNode function, and _v is the current view object. The updateRenderer function is called every time a change is detected, and its arguments _ck and _v are passed in.

The logic of the updateRenderer function is to get the current value from the binding property of the component object and call the _ck function, passing in the view object, the view node index, and the current value of the binding property. It is important to note that Angular performs DOM updates for each view, so you must pass in the view node index parameter. You can clearly see the _ck argument list:

function prodCheckAndUpdateNode(
    view: ViewData, 
    nodeIndex: number, argStyle: ArgumentType, v0? :any, v1? :any, v2? :any.Copy the code

NodeIndex is the index of the view node. If you have more than one expression in your template:

<h1>Hello {{name}}</h1>
<h1>Hello {{age}}</h1>
Copy the code

The compiler generates the following updateRenderer function:

var _co = _v.component;

// here node index is 1 and property is `name`
var currVal_0 = _co.name;
_ck(_v,1.0,currVal_0);

// here node index is 4 and bound property is `age`
var currVal_1 = _co.age;
_ck(_v,4.0,currVal_1);
Copy the code

Update the DOM

Now that we know all the objects generated by the Angular compiler (including the View, Element Node, Text Node, and updateRenderer items), we can explore how to update the DOM with these objects.

We know from the previous section that one of the arguments passed to the updateRenderer function during change detection is the _ck function, which is prodCheckAndUpdateNode. After continue to perform this function, you will call checkAndUpdateNodeInline, if the number of binding properties of more than 10, presents also provides checkAndUpdateNodeDynamic this function (note: the nature of two functions).

The checkAndUpdateNodeInline function executes the corresponding check update function based on the view node type:

case NodeFlags.TypeElement   -> checkAndUpdateElementInline
case NodeFlags.TypeText      -> checkAndUpdateTextInline
case NodeFlags.TypeDirective -> checkAndUpdateDirectiveInline
Copy the code

Let’s take a look at what these functions do. For NodeFlags.TypeDirective, check out my article The Mechanics of Property Bindings Update in Angular.

Note: Because this article focuses only on Element node and Text node.

Element nodes

For element nodes, will call a function and checkAndUpdateElementInline checkAndUpdateElementValue, CheckAndUpdateElementValue function checks whether binding form [attr. Name, class, name, style.css. Some] or attributes bind form:

case BindingFlags.TypeElementAttribute -> setElementAttribute
case BindingFlags.TypeElementClass     -> setElementClass
case BindingFlags.TypeElementStyle     -> setElementStyle
case BindingFlags.TypeProperty         -> setElementProperty;
Copy the code

The renderer method is then used to perform the corresponding operation on the node, such as adding a class to the current node span using setElementClass.

Text node

For text node types, checkAndUpdateTextInline is called, and here’s the main part:

if(checkAndUpdateBinding(view, nodeDef, bindingIndex, newValue)) { value = text + _addInterpolationPart(...) ; view.renderer.setValue(DOMNode, value); }Copy the code

It gets the current value passed in by the updateRenderer function. , compared with the value when the last change was detected. The view packet contains the oldValues property. If the property value, such as name, changes, Angular synthesizes the latest string text with the latest name value, such as Hello New World, and updates the corresponding text in the DOM with the renderer.

Note: Update element nodes and text nodes both mention renderers, which is also an important concept. Each view object has a Renderer property, which is a reference to Renderer2, the component renderer, which does the actual DOM updating. Because Angular is cross-platform, Renderer2 is an interface, so different renderers are selected for different platforms. For example, the Renderer is the DOMRenderer in the browser, the ServerRenderer on the server, and so on. As you can see here, the Angular framework design abstracts nicely.

conclusion

I know there’s a lot of hard information to digest, but once you understand it, you’ll be better able to design applications or debug DOM update-related issues. I recommend that you follow the source logic in this article, using debugger or debugger statements to debug the source code step by step.