# KnockoutJS: fein-granulare Reaktivität mit Observables und Computed

URL: https://www.mikebild.dev/de/blog/knockoutjs-observables-computed-vor-signals/

Drei Funktionen genügen, um in KnockoutJS eine ganze Oberfläche reaktiv zu halten: `ko.observable`, `ko.observableArray` und `ko.computed`. Keine davon nimmt eine Abhängigkeitsliste entgegen. Trotzdem weiß Knockout nach dem ersten Durchlauf, welcher Wert an welcher Stelle im DOM hängt. Es liest die Abhängigkeiten aus dem tatsächlichen Programmablauf ab.

**Redaktionelle Einordnung (Juli 2026):** Dieser Beitrag erklärt KnockoutJS als historischen Vorläufer heutiger fein-granularer Reaktivität. Ich empfehle damit nicht, 2026 ein neues Frontend auf Knockout aufzubauen. Interessant geblieben ist die Mechanik: Abhängigkeiten beim Lesen erfassen und nur die betroffenen Stellen aktualisieren.

Knockout koppelt das DOM über Bindings wie `text`, `value`, `foreach` und `click` deklarativ an ein ViewModel. Hier schaue ich eine Ebene tiefer: Wie merkt Knockout, dass sich etwas geändert hat, und wie findet es heraus, welche Stellen der Oberfläche davon betroffen sind? Der Kern dieser Mechanik heißt Observables und Computed.

## Ein Observable ist eine Funktion – keine Eigenschaft

Der erste gedankliche Sprung, den man bei Knockout machen muss, ist dieser: Ein Observable ist in KnockoutJS keine Eigenschaft, sondern eine Funktion. Genauer gesagt eine Funktion, die gleichzeitig Getter und Setter ist. Rufe ich sie ohne Argument auf, lese ich den Wert. Rufe ich sie mit einem Argument auf, schreibe ich den Wert – und Knockout benachrichtigt alle, die an diesem Wert interessiert sind.

```js
var name = ko.observable('Bob');

name();          // read  -> 'Bob'
name('Mary');    // write -> sets the value and notifies subscribers
name();          // read  -> 'Mary'
```

Das ist der ganze Trick. Kein `Object.defineProperty`, kein Proxy, keine Compiler-Magie. Nur eine Funktion, die sich merkt, wer sie gelesen hat und wer benachrichtigt werden will. Das Schreiben ist übrigens chainbar, weil der Setter das ViewModel zurückgibt:

```js
viewModel.name('Mary').age(50);
```

Für Listen gibt es die passende Variante. Ein `ko.observableArray` verhält sich beim Lesen wie ein Observable – `arr()` liefert das dahinterliegende Array – bringt aber zusätzlich Mutations-Methoden mit, die das Tracking auslösen:

```js
var todos = ko.observableArray([]);

todos();                 // read -> the current array
todos.push({ title: 'Buy milk' });
todos.remove(someItem);
todos.removeAll();
```

Hier lauert schon der erste Fallstrick, auf den ich später zurückkomme: Diese `push`-, `remove`- und `splice`-Methoden sind Knockout-eigene Wrapper. Sie sind es, die eine Benachrichtigung auslösen. Wer stattdessen `todos().push(x)` schreibt, greift am rohen Array vorbei und Knockout bekommt von der Änderung nichts mit.

## Computed leitet ab – und verfolgt automatisch, was es liest

Jetzt kommt der eigentlich interessante Teil. Selten will man einen Wert nur speichern und wieder ausgeben. Meistens braucht man abgeleitete Werte: die Summe eines Warenkorbs, den vollständigen Namen aus Vor- und Nachname, die Anzahl offener Aufgaben. Dafür gibt es `ko.computed`.

```js
var firstName = ko.observable('Mike');
var lastName  = ko.observable('Bild');

var fullName = ko.computed(function () {
  return firstName() + ' ' + lastName();
});

fullName();            // 'Mike Bild'
firstName('Michael');
fullName();            // 'Michael Bild'
```

Und hier passiert etwas, das ich mir beim ersten Kontakt mit Knockout selbst zweimal angeschaut habe, weil es der Dreh- und Angelpunkt des ganzen Modells ist. Ich habe Knockout nirgends gesagt, dass `fullName` von `firstName` und `lastName` abhängt. Ich habe keine Abhängigkeitsliste deklariert. Knockout hat das selbst herausgefunden.

Wie? Beim ersten Ausführen der Evaluator-Funktion beobachtet Knockout, welche Observables tatsächlich gelesen werden. `firstName()` und `lastName()` melden sich in diesem Moment gewissermaßen an: „Dieses Computed liest gerade mich.“ Knockout registriert daraufhin eine Subscription. Der Dependency-Graph entsteht damit zur Laufzeit, allein durchs Ausführen – deklariert wird nichts. Das ist das fein-granulare, automatische Dependency-Tracking, das Knockout ausmacht.

Der Evaluator läuft dabei nicht nur einmal. Er wird sofort bei der Erstellung einmal ausgeführt, und danach jedes Mal neu, wenn sich eine seiner gelesenen Abhängigkeiten ändert. Wichtig ist der zweite Teil: Bei jeder Neuauswertung ermittelt Knockout die Abhängigkeiten frisch. Das erlaubt dynamische Abhängigkeiten hinter Bedingungen:

```js
var showFullName = ko.observable(true);

var displayName = ko.computed(function () {
  if (showFullName()) {
    return firstName() + ' ' + lastName();
  }
  return firstName();
});
```

Solange `showFullName` auf `true` steht, hängt `displayName` von drei Observables ab. Steht es auf `false`, wird `lastName()` gar nicht mehr gelesen – und Knockout entfernt diese Abhängigkeit beim nächsten Lauf automatisch wieder. Eine Änderung an `lastName` löst dann keine Neuauswertung mehr aus. Genau das meint „dynamisch“: Der Graph passt sich dem tatsächlichen Kontrollfluss an.

Als Graph gezeichnet sieht dieser Zusammenhang so aus:

```mermaid
graph LR
  A[firstName<br/>observable] --> C[fullName<br/>computed]
  B[lastName<br/>observable] --> C
  C --> D[text-binding<br/>DOM]
```

Die Pfeile bedeuten „wird gelesen von und benachrichtigt“. Ändert sich `firstName`, wandert die Benachrichtigung entlang der Kante zu `fullName`, das sich neu berechnet, und von dort weiter zum Text-Binding im DOM. Nur dieser eine Pfad wird aktiv. Nichts anderes im Baum wird angefasst.

Für den Werkzeugkasten reicht fürs Erste dieses kleine Trio:

- `ko.observable(value)` – ein einzelner reaktiver Wert, gelesen mit `obs()`, geschrieben mit `obs(newValue)`.
- `ko.observableArray([...])` – eine reaktive Liste mit Mutations-Methoden wie `push`, `remove` und `splice`, die Bindings benachrichtigen.
- `ko.computed(fn)` – ein abgeleiteter Wert, der automatisch verfolgt, welche Observables er liest, und sich bei deren Änderung neu berechnet.

## Vom ViewModel ins DOM: die Todos-App

Damit das nicht abstrakt bleibt, hier eine kleine Todos-Anwendung. Sie zeigt alle drei Bausteine im Zusammenspiel.

```js
function TodoViewModel() {
  var self = this;

  self.todos = ko.observableArray([]);
  self.newTodo = ko.observable('');

  self.remaining = ko.computed(function () {
    return self.todos().filter(function (t) {
      return !t.done();
    }).length;
  });

  self.addTodo = function () {
    self.todos.push({
      title: self.newTodo(),
      done: ko.observable(false)
    });
    self.newTodo('');
  };
}

ko.applyBindings(new TodoViewModel());
```

Ein paar Dinge sind hier bewusst so gebaut. `self.todos` ist ein `observableArray`, `self.newTodo` ein einfaches Observable für das Eingabefeld. `self.remaining` ist ein Computed, das die noch offenen Aufgaben zählt. Es liest `self.todos()` und pro Eintrag `t.done()` – und damit hängt es sowohl an der Liste als auch an jedem einzelnen `done`-Observable. Fügt man ein Todo hinzu oder hakt eines ab, rechnet `remaining` von selbst neu.

Beachtenswert ist, dass jedes einzelne Todo ein eigenes `done: ko.observable(false)` bekommt. Nur so ist der Erledigt-Status pro Eintrag reaktiv. Ein reines `done: false` wäre ein toter Wert, an dem sich nichts beobachten ließe.

Die View koppelt das ViewModel deklarativ ans DOM. Hier der relevante Ausschnitt:

```html
<input data-bind="value: newTodo" />
<button data-bind="click: addTodo">Add</button>

<ul data-bind="foreach: todos">
  <li data-bind="text: title"></li>
</ul>

<span data-bind="text: remaining"></span> remaining
```

`ko.applyBindings` aktiviert diese Verknüpfungen einmalig beim Start. Ab da läuft alles über das Tracking: Tippt der Nutzer ins Feld, aktualisiert das `value`-Binding `newTodo`. Ein Klick auf den Button ruft `addTodo` auf, das per `push` die Liste verändert. Diese Änderung benachrichtigt das `foreach`-Binding (neuer Listeneintrag) und das Computed `remaining` (Neuberechnung), das wiederum das `text`-Binding im `<span>` aktualisiert. Jede dieser Aktualisierungen trifft gezielt genau die Stelle, die betroffen ist.

## Warum gezieltes Tracking ein anderes Modell ist

An diesem Punkt lohnt sich die Abgrenzung, die dem Ganzen erst Kontur gibt. Es gibt einen zweiten, sehr verbreiteten Ansatz für dasselbe Problem: das Virtual-DOM-Diffing, wie man es etwa von React kennt. Dort baut man bei jeder Änderung einen neuen virtuellen Baum, vergleicht ihn mit dem alten und wendet die Differenz aufs echte DOM an. Das Framework weiß nicht von vornherein, welche Datenänderung welche DOM-Stelle betrifft – es rechnet den Unterschied nachträglich aus.

Knockout geht den umgekehrten Weg. Durch das fein-granulare Tracking weiß es pro Observable exakt, welche Computed und welche Bindings davon abhängen. Es muss nichts vergleichen. Ändert sich ein Wert, folgt Knockout einfach den registrierten Kanten des Dependency-Graphen und aktualisiert nur die dort hängenden DOM-Stellen. Kein Baum-Vergleich, kein Neu-Rendern von Teilbäumen, die sich gar nicht geändert haben.

Die Sequenz einer einzelnen Änderung sieht so aus:

```mermaid
sequenceDiagram
  participant U as User
  participant O as observable
  participant C as computed
  participant D as DOM-Binding
  U->>O: obs(newValue)
  O->>C: notify subscriber
  C->>C: re-evaluate
  C->>D: gezieltes text-Update
```

Beide Modelle haben ihre Berechtigung, und ich möchte keines zum Sieger erklären. Aber wer den Unterschied einmal an dieser Stelle verstanden hat, versteht auch, warum die beiden Welten sich so unterschiedlich anfühlen. Knockouts Ansatz ist unmittelbar nachvollziehbar, weil man den Graphen im Code fast sehen kann. Der Preis dafür ist die Disziplin: Man muss konsequent mit Observables arbeiten, sonst reißt der Graph an einer Stelle ab.

## Fallstricke aus der Praxis

Genau an dieser Disziplin reißt der Graph gern, und zwar an drei wiederkehrenden Stellen. Alle drei haben mich in eigenen Projekten schon Stunden gekostet, weil sie stillschweigend danebengehen.

Der Klassiker sind vergessene Klammern. Ein Observable ist eine Funktion. Wer sie ohne Klammern verwendet, arbeitet mit der Funktion selbst statt mit ihrem Wert:

```js
// wrong: todo.done is the function itself, so always truthy
if (todo.done) {
  // this branch ALWAYS runs
}

// correct: todo.done() reads the value
if (todo.done()) {
  // ...
}
```

Der Fehler ist tückisch, weil er keinen Fehler wirft. Eine Funktion ist truthy, also läuft die Bedingung schlicht immer ins gleiche Ergebnis. Man sucht dann lange an der falschen Stelle. Zum Lesen gehören immer Klammern, ohne Ausnahme.

Der zweite Fallstrick sind die nativen Array-Methoden. Ich hatte es oben angedeutet:

```js
// wrong: mutates the raw array without notification
todos().push(newItem);

// correct: KO wrapper, notifies dependent bindings
todos.push(newItem);
```

`todos()` liefert das darunterliegende JavaScript-Array. Dessen natives `push` verändert zwar den Inhalt, aber Knockout erfährt nichts davon – das `foreach`-Binding bleibt stehen. Der KO-Wrapper `todos.push` hingegen löst das Tracking aus. Der Unterschied liegt einzig in den Klammern vor dem `.push`, was den Fehler wieder unangenehm subtil macht.

Der dritte betrifft den `this`-Kontext in Computed. Wenn der Evaluator auf das ViewModel zugreifen will, muss `this` auch wirklich aufs ViewModel zeigen:

```js
function ViewModel() {
  this.price = ko.observable(10);
  this.count = ko.observable(3);

  // wrong: this does not reliably point to the ViewModel here
  this.total = ko.computed(function () {
    return this.price() * this.count();   // this.price is undefined
  });
}
```

Der Evaluator wird von Knockout in einem anderen Kontext aufgerufen, sodass `this` nicht auf die Instanz zeigt. Es gibt zwei etablierte Auswege. Entweder man reicht den Kontext als zweites Argument nach:

```js
this.total = ko.computed(function () {
  return this.price() * this.count();
}, this);   // second argument binds this
```

Oder man verwendet das `self`-Muster, das ich im Todos-Beispiel schon genutzt habe:

```js
function ViewModel() {
  var self = this;
  self.price = ko.observable(10);
  self.count = ko.observable(3);
  self.total = ko.computed(function () {
    return self.price() * self.count();
  });
}
```

Ich bevorzuge in meinen Projekten das `self`-Muster, weil es durchgängig funktioniert – auch in Event-Handlern und Callbacks, nicht nur in Computed. Ein Muster für alles ist leichter zu merken als eine Sonderregel pro Fall.

Am Rande sei noch `ko.pureComputed` erwähnt, das seit KO 3.2 zur Verfügung steht. Es verhält sich wie ein normales Computed, „schläft“ aber, solange niemand es abonniert, und spart so Neuberechnungen bei reinen Ableitungen ohne Seiteneffekte. Für den Einstieg reicht `ko.computed`; sobald man merkt, dass ein rein ableitendes Computed unnötig oft rechnet, ist `pureComputed` die naheliegende Verbesserung.

## Fazit

Wer den Kern von KnockoutJS verstanden hat, hält im Grunde nur drei Bausteine in der Hand: Observables als get/set-Funktionen, observableArrays für reaktive Listen und Computed für abgeleitete Werte. Das eigentlich Elegante liegt zwischen diesen Bausteinen: im automatischen Dependency-Tracking. Knockout beobachtet zur Laufzeit, welche Observables ein Computed liest, baut daraus einen Dependency-Graphen und aktualisiert bei jeder Änderung gezielt nur die betroffenen Stellen – neu ermittelt bei jedem Lauf, damit auch dynamische Abhängigkeiten hinter Bedingungen sauber abgebildet werden.

Dieses Modell steht bewusst quer zum Virtual-DOM-Diffing: Statt Bäume zu vergleichen, folgt Knockout den Kanten, die es beim Lesen selbst registriert hat. Das macht das Verhalten unmittelbar nachvollziehbar, verlangt aber Disziplin – Klammern zum Lesen, KO-Wrapper zum Mutieren von Listen, den richtigen `this`-Kontext im Computed. Sind diese drei Reflexe verinnerlicht, wird das Arbeiten mit Knockout überraschend ruhig. Man beschreibt, wovon ein Wert abhängt, und das Framework kümmert sich um den Rest. Genau diese Klarheit ist der Grund, warum ich Knockouts Reaktivitätsmodell nach wie vor für einen der besten Einstiege halte, um zu begreifen, was hinter reaktiven Oberflächen wirklich passiert.

## Weiterführende Quellen

- [Observables – KnockoutJS Documentation](https://knockoutjs.com/documentation/observables.html)
- [Computed Observables – KnockoutJS Documentation](https://knockoutjs.com/documentation/computedObservables.html)
- [How Dependency Tracking Works – KnockoutJS Documentation](https://knockoutjs.com/documentation/computed-dependency-tracking.html)
- [Observable Arrays – KnockoutJS Documentation](https://knockoutjs.com/documentation/observableArrays.html)
- [Pure Computed Observables – KnockoutJS Documentation](https://knockoutjs.com/documentation/computed-pure.html)
