You write this.identify="Dwayne" in a plain class with no decorators, no base class and no setState, and the web page updates. There’s no digest loop working within the background and no digital DOM being diffed. Aurelia discovered about that task the second it occurred. It does that with a handful of mechanisms, and the identical mechanisms clarify each binding that refuses to replace.
This submit walks by every one, utilizing the @aurelia/runtime supply because the map, and finishes with the modifications Aurelia can’t see and what to do about them.
The primary learn
Take this template:
When the binding behind ${person.nickname} is sure, it evaluates the expression as soon as to get the preliminary worth. Analysis doesn’t simply return a worth, although. Each time it reads a property (person from the view mannequin, then nickname from person), it asks for an observer for that object and key and subscribes to it.
These observers come from IObserverLocator, and nearly all the fascinating selections reside in its createObserver technique. So as, it checks:
- Is the goal a DOM node? Then
runtime-htmlprovides an observer that listens to occasions, corresponding toenterandchangefor a textual content field’sworth. - Is it
sizeon an array,measurementon a Map or Set, or a numeric index on an array? Then it will get a set observer. - Does the property have a getter? Then it will get a computed observer, or a unclean checker if the getter can’t get replaced. Each are lined beneath.
- Anything, which is almost each property you’ll ever bind to, will get a
SetterObserver.
Getters and setters on plain properties
A SetterObserver does one factor. The primary time one thing subscribes, it reads the present worth and replaces the property with a getter and setter pair:
// Simplified from setter-observer.ts
begin() {
this._value = this._obj[this._key];
Object.defineProperty(this._obj, this._key, {
enumerable: true,
configurable: true,
get: () => this._value,
set: (worth) => this.setValue(worth),
});
}
setValue(newValue) {
if (Object.is(newValue, this._value)) return;
const oldValue = this._value;
this._value = newValue;
this.subs.notify(newValue, oldValue);
}
That’s the entire trick behind plain class statement. After the binding is ready up, person.nickname is now not an information property. In the event you examine it, you’ll discover get and set the place worth was once:
Object.getOwnPropertyDescriptor(person, 'nickname');
// { get: f, set: f, enumerable: true, configurable: true }
A number of issues comply with from this:
- The property doesn’t have to exist but. If
personhas nonicknamewhen the template binds, the observer installs the accessor anyway, and a laterperson.nickname="Dee"updates the web page. - Solely the properties a binding reads are touched. The remainder of the item is left alone, and nothing is wrapped up entrance.
- Setting the identical worth does nothing. The comparability is
Object.is, so assigning an similar string or the identical object reference received’t notify anybody. - Your object is rarely changed. The binding holds the identical
personyou created, so code exterior Aurelia that retains a reference to it sees the identical object and goes by the identical setter.
Setting a worth doesn’t contact the DOM straight away
Notification is synchronous. When the setter runs, each subscriber hears about it earlier than the task returns. The DOM write just isn’t:
element.person.nickname = 'Dee';
appHost.querySelector('p').textContent; // nonetheless the outdated worth
await tasksSettled();
appHost.querySelector('p').textContent; // 'Dee'
Bindings that write to the DOM (textual content content material, attributes, component properties, class and model) queue their replace with queueTask as an alternative of writing immediately. Every binding queues itself as soon as, so ten assignments in a row produce one DOM write with the ultimate worth. Bindings right into a customized component’s bindable usually are not DOM writes, so that they move the worth by instantly, and the kid’s nameChanged callback runs synchronously.
The delay is why assessments await tasksSettled() (from @aurelia/runtime) after altering state. It’s additionally why studying structure straight after an task offers you stale numbers.
Getters: monitoring with a proxy
Getters can’t be dealt with the identical approach. There’s no worth to intercept, solely a operate, and Aurelia can’t know what it is dependent upon till it runs it. In order that’s what it does:
export class TodoList {
todos = [
{ title: 'Write post', done: false },
{ title: 'Publish post', done: false },
];
get doneCount() {
return this.todos.filter(t => t.completed).size;
}
}
When the binding reads doneCount, the observer locator finds a getter on the prototype and creates a ComputedObserver. That observer calls your getter with this set to a proxy of your view mannequin, and the proxy data each property learn whereas the getter runs:
this.todossubscribes totodoson the view mannequin, and returns the array wrapped in a proxy too..filter(...)on the wrapped array subscribes to the array as a set, and fingers every merchandise to your callback wrapped as properly.t.completedsubscribes tocompletedon every todo.
Change any of these (this.todos = [], todos.push(...), todos[0].completed = true) and the getter runs once more.
The proxy solely collects dependencies. It has no set entice, and it isn’t the factor that notices modifications. If you write todos[0].completed = true, that task hits the getter and setter {that a} SetterObserver put in on the actual todo object, precisely as within the earlier part. The proxy is simply there to seek out out which properties want these observers.
Dependencies are collected recent on each run, and something the most recent run didn’t learn is unsubscribed. That retains branches low-cost:
get seen() {
return this.showAll ? this.todos : this.todos.filter(t => !t.completed);
}
Whereas showAll is true, this getter doesn’t subscribe to every todo’s completed, as a result of the final run by no means learn it.
The proxy wraps plain objects, arrays, Maps and Units. It doesn’t wrap Dates, or the rest whose inner state lives in slots somewhat than properties, and that comes up once more later. In the event you want management as an alternative of auto-tracking, @computed takes specific dependencies, deep, and a flush possibility for when the getter recomputes. That’s a submit of its personal.
Collections: patched strategies and index maps
Assigning a brand new array to this.objects goes by a setter like the rest. this.objects.push(x) doesn’t assign something, so the setter by no means runs.
For this, Aurelia patches the mutating strategies on Array.prototype: push, pop, shift, unshift, splice, reverse and kind. It does the identical for set, delete and clear on Map.prototype, and add, delete and clear on Set.prototype. Patching occurs as soon as, the primary time any assortment is noticed. Every patched technique appears to be like up the array in a WeakMap of noticed collections. If it isn’t there, the native technique runs and nothing else occurs, so arrays your templates by no means contact pay for one map lookup.
When the array is noticed, the patched technique does the mutation itself and data an index map because it goes: for every place within the new array, which place it got here from, or a marker which means the merchandise is new, plus an inventory of deleted indices. repeat.for makes use of that map to maneuver, add and take away the prevailing DOM nodes as an alternative of rendering the checklist once more. That’s why sorting an inventory of a thousand objects in place strikes nodes somewhat than recreating them.
Templates also can name read-only array strategies and keep updated:
<p if.bind="selectedIds.consists of(merchandise.id)">Chosen</p>
<p>${objects.filter(i => i.completed).size} completed</p>
When an expression calls consists of, filter, map, discover, some, each, slice, be part of, cut back or the opposite read-only array strategies, the binding subscribes to the array itself, so a later push updates it.
Maps and Units don’t get that therapy in templates. ${scores.get('a')} and ${tags.has('x')} present the suitable worth after they bind, then by no means change, even after scores.set('a', 5). ${scores.measurement} and repeat.for="[key, value] of scores" do replace. For lookups, use a getter. Inside a getter, the proxy tracks get and has correctly:
get scoreA() {
return this.scores.get('a');
}
Technique calls in templates
Getters are tracked. Abnormal strategies referred to as from a template aren’t:
export class Profile {
first = 'Dwayne';
final = 'Charrington';
fullName() {
return `${this.first} ${this.final}`;
}
}
This renders accurately, then stays that approach ceaselessly. The binding observes the strategy’s arguments, and there aren’t any. It doesn’t run the strategy below a proxy, so it has no concept that first and final matter. You might have 3 ways out:
- Flip it right into a getter, if it takes no arguments.
- Move what it is dependent upon:
${fullName(first, final)}. Arguments are extraordinary expressions and are noticed. - Enhance it with
@computed. Since RC.1,@computedworks on strategies, and a adorned technique referred to as from a template is tracked the identical approach a getter is.
import { computed } from 'aurelia';
export class Profile {
first = 'Dwayne';
final = 'Charrington';
@computed
fullName() {
return `${this.first} ${this.final}`;
}
}
Occasion handlers like click on.set off="save()" aren’t affected by any of this. They run when the occasion fires, not when knowledge modifications.
Soiled checking, the final resort
There’s one case the place Aurelia can neither set up a setter nor safely change a getter: an accessor that isn’t configurable. The computed observer works by redefining the property, and Object.defineProperty refuses to the touch a non-configurable one. With nothing to hook into, the observer locator fingers the property to the soiled checker.
The soiled checker polls. It retains an inventory of those properties and compares every one in opposition to its final recognized worth on a timer, roughly each 100ms in a foreground tab. Background tabs throttle timers, so checks there are a lot much less frequent. A change reveals up late, and each dirty-checked property prices a learn per verify whether or not or not something modified.
You hardly ever create one on objective, however they flip up. Object.defineProperty defaults configurable to false, so a getter outlined with out spelling it out finally ends up soiled checked:
Object.defineProperty(Account.prototype, 'steadiness', {
get() { return this.credit - this.debits; },
// configurable: true is lacking, so this property will get soiled checked
});
Class getters are configurable, so get steadiness() in a category physique is okay. Libraries that outline their very own accessors are the extra widespread supply.
To search out them, activate the throw setting throughout improvement:
import { DirtyCheckSettings } from '@aurelia/runtime';
DirtyCheckSettings.throw = true;
Now any binding that might fall again to polling throws as an alternative:
AUR0218: Soiled checked just isn't permitted on this software. Property key steadiness is being soiled checked.
DirtyCheckSettings additionally has timeoutsPerCheck to alter the polling charge, and disabled to cease polling fully. Disabling it means these properties are merely by no means up to date, which is worse than gradual.
The modifications Aurelia can’t see
All the pieces above is dependent upon a change going by a setter or a patched technique. Something that doesn’t is invisible.
Writing to an array index or size out of your code. this.objects[0] = 'z' and this.objects.size = 0 are native operations on the array. Aurelia doesn’t use a proxy in your arrays exterior of getters, so it by no means hears about them, and neither the repeat nor ${objects[0]} updates. Use splice:
this.objects.splice(0, 1, 'z'); // change the primary merchandise
this.objects.splice(0, this.objects.size); // clear
This issues for multiple stale worth. After objects.size = 0, the repeat nonetheless has views for the outdated objects. The subsequent actual mutation fingers it an index map that doesn’t match what it rendered, and it throws AUR0814 (variety of views != variety of objects). fill and copyWithin aren’t patched both, so that they have the identical downside.
Expressions within the template are a unique story. click on.set off="objects[0] = 'z'" goes by Aurelia’s expression evaluator, which notifies the array observer itself, in order that works.
Mutating a Date. due.setDate(15) modifications the Date’s inner slot, not a property, so nothing is listening. Construct a brand new Date and assign it:
const due = new Date(this.due);
due.setDate(15);
this.due = due;
Deleting a property. delete person.nickname removes the accessor the SetterObserver put in. Assign person.nickname later and also you get a plain knowledge property once more, and the binding by no means hears about it. Set it to undefined or null as an alternative.
Studying #non-public fields in a getter. A tracked getter runs with this set to a proxy, and JavaScript doesn’t permit studying a non-public discipline by a proxy. A getter that returns this.#rely fails with Can't learn non-public member #rely from an object whose class didn't declare it. Use TypeScript’s non-public key phrase for state a template is dependent upon. It’s solely a sort verify and doesn’t change the runtime object.
State exterior your objects. A worth learn from localStorage, a module-level variable, or something a third-party library retains in a closure doesn’t have a property Aurelia can connect to. Copy it onto your view mannequin when it modifications, normally from the library’s personal occasion or callback.
Reacting in code
Bindings aren’t the one issues that may subscribe. A number of instruments give your individual code a approach in:
@observableon a discipline installs the getter and setter when the occasion is created, not when a binding first reads the property, and calls afieldNameChanged(newValue, oldValue)technique on each change. It really works whether or not or not something in a template is sure to the sphere.@watchruns a callback when an expression or a operate’s dependencies change. The operate kind is tracked by the identical proxy getters use.IObserverLocatoroffers you the observers straight, bygetObserver(obj, key),getArrayObserver(arr)and the remaining, when it’s essential to subscribe from one thing that isn’t a element.
batch() from aurelia helps when a number of assignments belong collectively. Notifications are synchronous, so a subscriber that is dependent upon three properties hears about every task individually. Inside batch(() => { ... }), notifications are held till the callback returns, and every subscriber is informed as soon as with the ultimate values.
When a binding received’t replace
Most stale bindings come all the way down to considered one of these:
- The change didn’t undergo a setter: an index or
sizewrite,fill, a mutated Date, or a deleted property. - A template calls a way that reads state the binding can’t see. Make it a getter, move the dependencies as arguments, or add
@computed. - A template calls
getorhason a Map or Set. Transfer the lookup right into a getter. - A getter reads
#non-publicstate. - The worth lives someplace exterior your view mannequin.
- The binding is okay and the DOM write hasn’t occurred but. In a check,
await tasksSettled().
If none of those match, observer-locator.ts in packages/runtime/src is below 300 strains and makes a very good place to begin studying. Or carry it to
Discord
, the place any individual has nearly definitely hit the identical factor.

