Wednesday, October 14, 2026
HomeJavaScriptHow Aurelia Is aware of One thing Modified

How Aurelia Is aware of One thing Modified


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:

  1. Is the goal a DOM node? Then runtime-html provides an observer that listens to occasions, corresponding to enter and change for a textual content field’s worth.
  2. Is it size on an array, measurement on a Map or Set, or a numeric index on an array? Then it will get a set observer.
  3. 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.
  4. 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 person has no nickname when the template binds, the observer installs the accessor anyway, and a later person.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 person you 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.todos subscribes to todos on 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.completed subscribes to completed on 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, @computed works 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:

  • @observable on a discipline installs the getter and setter when the occasion is created, not when a binding first reads the property, and calls a fieldNameChanged(newValue, oldValue) technique on each change. It really works whether or not or not something in a template is sure to the sphere.
  • @watch runs a callback when an expression or a operate’s dependencies change. The operate kind is tracked by the identical proxy getters use.
  • IObserverLocator offers you the observers straight, by getObserver(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:

  1. The change didn’t undergo a setter: an index or size write, fill, a mutated Date, or a deleted property.
  2. 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.
  3. A template calls get or has on a Map or Set. Transfer the lookup right into a getter.
  4. A getter reads #non-public state.
  5. The worth lives someplace exterior your view mannequin.
  6. 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.

RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Most Popular

Recent Comments