Inside Angular's DOM Update Mechanism
Model-driven DOM updates are the cornerstone of every modern front-end framework, and Angular is no exception. We write expressions like:
<span>Hello {{name}}</span>
or bindings such as:
<span [textContent]="'Hello ' + name"></span>
and Angular seamlessly refreshes the DOM when the name property changes. What appears straightforward externally involves significant internal complexity. DOM updating is a component of Angular's change detection system, which primarily comprises three key operations:
- DOM updates
- child components
Inputbindings updates - query list updates
This piece examines how the rendering aspect of change detection operates. For those curious about how this is achieved, read ahead. I'll reference code under the assumption the app runs in production mode. Let's dive in.
How the Application Is Structured Internally
Before diving into Angular's DOM update capabilities, you need to grasp how an Angular application is organized underneath. We'll take a quick look at that.
View
As covered in earlier articles, for every component in the app, the Angular compiler produces a factory. When Angular constructs a component from that factory, for instance:
const factory = r.resolveComponentFactory(AComponent);
factory.create(injector);
the factory is used to instantiate a View Definition, which then leads to creating a component View. Beneath the surface, Angular models the app as a hierarchy of views. For each component type, there is a single view definition instance that acts as a blueprint for every view. Yet each component instance gets its own separate view.
Factory
A component factory is largely composed of the view nodes that the compiler generates from parsing the template. Consider the following component template:
<span>I am {{name}}</span>
From this, the compiler produces this component factory:
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);
This captures the component view's layout and is utilized during instantiation. Here, jit_viewDef1 points to the viewDef function that constructs the view definition.
That view definition accepts view definition nodes as arguments, which mirror the HTML structure but include numerous Angular-specific details. In the example above, the initial node jit_elementDef2 represents an element definition, while jit_textDef3 is a text definition. The Angular compiler produces a variety of node definitions, and the node's category is recorded in the NodeFlags. Later, we'll see how Angular leverages this node type information to choose the appropriate update strategy.
For this discussion, we'll focus only on element and text nodes:
export const enum NodeFlags {
TypeElement = 1 << 0,
TypeText = 1 << 1
Let's go over them quickly.
Element definition
An element definition is a node Angular generates for each HTML tag. This node type is also created for components. Element nodes may have other element nodes and text definition nodes as children, as indicated by the childCount property.
Every element definition is created through the elementDef function, so jit_elementDef2 in the factory refers to it. The element definition takes some 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 |
+------------------+-----------------------------------+
and additional ones tied to specific Angular features:
+----------------------+------------------------------------------+
| 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 |
+----------------------+------------------------------------------+
For our purposes, we care most about the bindings parameters.
Text definition
A text definition is a node created for every text node. Typically, these are descendants of element definition nodes, as seen in our example. This simple node type is generated by the textDef function. It takes parsed expressions as constants in its second parameter. For instance, this text:
<h1>Hello {{name}} and another {{prop}}</h1>
gets parsed into an array:
["Hello ", " and another ", ""]
which is subsequently used to produce the proper bindings:
{
text: 'Hello',
bindings: [
{
name: 'name',
suffix: ' and another '
},
{
name: 'prop',
suffix: ''
}
]
}
and evaluated during dirty checking like this:
text
+ context[bindings[0][property]] + context[bindings[0][suffix]]
+ context[bindings[1][property]] + context[bindings[1][suffix]]
Node definition bindings
Angular employs bindings to tie each node's dependencies to the component class properties. During change detection, each binding dictates the operation Angular uses to refresh the node and supplies contextual details. The operation type is determined by binding flags, and for DOM-specific actions, this includes:
+-----------------------+--------------------------+
| Name | Construction in template |
+-----------------------+--------------------------+
| TypeElementAttribute | attr.name |
| TypeElementClass | class.name |
| TypeElementStyle | style.name |
+-----------------------+--------------------------+
Element and text definitions construct these bindings internally based on the flags identified by the compiler. Each node type applies its own logic when creating bindings.
The update renderer
What really matters here is the function at the end of the factory View_AComponent_0 that the compiler generates:
function(_ck,_v) {
var _co = _v.component;
var currVal_0 = _co.name;
_ck(_v,1,0,currVal_0);
That function goes by updateRenderer. It accepts two arguments: _ck and v. The _ck stands for check and points to the function prodCheckAndUpdate. The second parameter is the component's view holding the nodes. The updateRenderer function runs each time change detection is triggered for a component, with the parameters supplied by that mechanism.
Its primary job is to fetch the bound property's current value from the component instance and pass the view, node index, and that value to _ck. A crucial point: Angular processes DOM updates for each view node independently, which is why the node index exists. Looking at the parameter list for the function behind _ck makes this clear:
function prodCheckAndUpdateNode(
view: ViewData,
nodeIndex: number,
argStyle: ArgumentType,
v0?: any,
v1?: any,
v2?: any,
Here, nodeIndex identifies the specific view node to check and update. For a template containing several expressions:
<h1>Hello {{name}}</h1>
<h1>Hello {{age}}</h1>
the compiler produces an updateRenderer function body like:
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);
Refreshing the DOM
Armed with the specific objects Angular's compiler generates, we can now examine how DOM updates actually happen with them.
Earlier we saw that updateRenderer gets a _ck function during change detection, referencing prodCheckAndUpdate. This compact, general-purpose function performs a series of calls that eventually land on the checkAndUpdateNodeInline function. For cases where expressions exceed 10, a variation of that function exists.
The checkAndUpdateNode function serves as a dispatcher that distinguishes among view node types and directs the check and update to the right handler:
case NodeFlags.TypeElement -> checkAndUpdateElementInline
case NodeFlags.TypeText -> checkAndUpdateTextInline
case NodeFlags.TypeDirective -> checkAndUpdateDirectiveInline
Now, let's inspect those handlers. For NodeFlags.TypeDirective, refer to The mechanics of property bindings update in Angular.
Type Element
The handler here is CheckAndUpdateElement. Its role is to identify whether the binding uses Angular's special forms like [attr.name, class.name, style.some] or targets a node-specific property.
case BindingFlags.TypeElementAttribute -> setElementAttribute
case BindingFlags.TypeElementClass -> setElementClass
case BindingFlags.TypeElementStyle -> setElementStyle
case BindingFlags.TypeProperty -> setElementProperty;
Based on that, the corresponding method on the renderer is invoked to apply the needed change.
Type Text
Here, both variants rely on the function CheckAndUpdateText. The core logic looks like:
if (checkAndUpdateBinding(view, nodeDef, bindingIndex, newValue)) {
value = text + _addInterpolationPart(...);
view.renderer.setValue(DOMNode, value);
}
Essentially, it takes the current value provided by updateRenderer and compares it against what was stored from the last change detection run. Those old values are kept in the View's oldValues property. When a mismatch is detected, Angular takes the new value, builds a string, and updates the DOM via the renderer.
Wrapping up
Admittedly, that's a lot to take in. Still, having this understanding puts you in a stronger position when designing apps or troubleshooting DOM update problems. I'd also recommend using a debugger to trace through the execution flow described here.
