Viele JavaScript-Front-End-Frameworks (wie Angular, React und Vue) verfügen über eigene Reaktivitätssysteme. Das Verständnis der Funktionen dieser Systeme ist für jeden Entwickler hilfreich und hilft ihm, moderne JS-Frameworks effizienter zu nutzen.

Das Material, dessen Übersetzung wir heute veröffentlichen, zeigt ein schrittweises Beispiel für die Entwicklung eines Reaktivitätssystems in reinem JavaScript. Dieses System implementiert dieselben Mechanismen, die in Vue verwendet werden.
Reaktivitätssystem
Für jemanden, der zum ersten Mal auf das Vue-Reaktivitätssystem stößt, mag es wie eine mysteriöse Black Box erscheinen. Betrachten Sie eine einfache Vue-Anwendung. Hier ist das Markup:
<div id="app"> <div>Price: ${{ price }}</div> <div>Total: ${{ price*quantity }}</div> <div>Taxes: ${{ totalPriceWithTax }}</div> </div>
Hier ist der Framework-Verbindungsbefehl und der Anwendungscode.
<script src="https://cdn.jsdelivr.net/npm/vue"></script> <script> var vm = new Vue({ el: '#app', data: { price: 5.00, quantity: 2 }, computed: { totalPriceWithTax() { return this.price * this.quantity * 1.03 } } }) </script>
Irgendwie findet Vue heraus, dass der Motor drei Dinge tun muss, wenn sich der
price
ändert:
- Aktualisieren Sie den
price
auf der Webseite. - Berechnen Sie den Ausdruck, in dem der
price
mit der quantity
multipliziert wird, neu und zeigen Sie den resultierenden Wert auf der Seite an. - Rufen Sie die Funktion
totalPriceWithTax
und setzen Sie erneut das, was sie zurückgibt, auf die Seite.
Was hier passiert, ist in der folgenden Abbildung dargestellt.
Woher weiß Vue, was zu tun ist, wenn sich die Preiseigenschaft ändert?Jetzt haben wir Fragen dazu, wie Vue weiß, was genau aktualisiert werden muss, wenn sich der
price
ändert, und wie die Engine verfolgt, was auf der Seite passiert. Was Sie hier beobachten können, sieht nicht wie eine normale JS-Anwendung aus.
Vielleicht ist dies noch nicht offensichtlich, aber das Hauptproblem, das wir hier lösen müssen, ist, dass JS-Programme normalerweise nicht so funktionieren. Führen Sie beispielsweise den folgenden Code aus:
let price = 5 let quantity = 2 let total = price * quantity
Was wird Ihrer Meinung nach in der Konsole angezeigt? Da hier außer regulärem JS nichts verwendet wird, gelangt
10
zur Konsole.
Das Ergebnis des ProgrammsUnd wenn wir die Funktionen von Vue verwenden, können wir in einer ähnlichen Situation ein Szenario implementieren, in dem der
total
nachgezählt wird, wenn sich die
price
oder
quantity
ändern. Das heißt, wenn das Reaktivitätssystem bei der Ausführung des obigen Codes verwendet würde, würden nicht 10, sondern 40 auf der Konsole angezeigt:
Konsolenausgabe, die durch hypothetischen Code unter Verwendung eines Reaktivitätssystems generiert wirdJavaScript ist eine Sprache, die sowohl prozedural als auch objektorientiert funktionieren kann, jedoch kein integriertes Reaktivitätssystem enthält. Daher gibt der Code, den wir bei der
price
berücksichtigt haben, die Nummer 40 nicht an die Konsole aus. Damit der
total
neu berechnet werden kann, wenn sich der
price
oder die
quantity
ändert, müssen wir selbst ein Reaktivitätssystem erstellen und damit das gewünschte Verhalten erreichen. Wir werden den Weg zu diesem Ziel in mehrere kleine Schritte unterteilen.
Aufgabe: Speicherung von Regeln zur Berechnung von Indikatoren
Wir müssen irgendwo Informationen darüber speichern, wie der
total
berechnet wird, damit wir ihn neu berechnen können, wenn wir die Werte der
price
oder
quantity
ändern.
▍Lösung
Zuerst müssen wir der Anwendung Folgendes mitteilen: "Hier ist der Code, den ich ausführen werde. Speichern Sie ihn. Möglicherweise muss ich ihn ein anderes Mal ausführen." Dann müssen wir den Code ausführen. Wenn sich später die
price
oder
quantity
geändert haben, müssen Sie den gespeicherten Code aufrufen, um die
total
neu zu berechnen. Es sieht so aus:
Der gesamte Berechnungscode muss irgendwo gespeichert werden, um später darauf zugreifen zu könnenDer Code, den Sie in JavaScript aufrufen können, um eine Aktion auszuführen, ist als Funktion formatiert. Daher werden wir eine Funktion schreiben, die sich mit der Berechnung der
total
, und einen Mechanismus zum Speichern von Funktionen erstellen, die wir später möglicherweise benötigen.
let price = 5 let quantity = 2 let total = 0 let target = null target = function () { total = price * quantity } record()
Beachten Sie, dass wir die anonyme Funktion in der
target
speichern und dann die
record
aufrufen. Wir werden weiter unten darüber sprechen. Ich möchte auch darauf hinweisen, dass die
target
unter Verwendung der Syntax der ES6-Pfeilfunktionen wie folgt umgeschrieben werden kann:
target = () => { total = price * quantity }
Hier ist die Deklaration der
record
und der Datenstruktur, die zum Speichern der Funktionen verwendet wird:
let storage = [] // target function record () { // target = () => { total = price * quantity } storage.push(target) }
Mit der
record
speichern wir die
target
(in unserem Fall
{ total = price * quantity }
) im
storage
, wodurch wir diese Funktion später aufrufen können, möglicherweise mit der
replay
, deren Code unten gezeigt wird. Dadurch können wir alle im
storage
gespeicherten Funktionen aufrufen.
function replay () { storage.forEach(run => run()) }
Hier gehen wir alle anonymen Funktionen durch, die im
storage
gespeichert sind, und führen jede von ihnen aus.
Dann können wir in unserem Code Folgendes tun:
price = 20 console.log(total) // 10 replay() console.log(total) // 40
Es sieht nicht alles so schwer aus, oder? Hier ist der gesamte Code, dessen Fragmente wir oben besprochen haben, falls es für Sie bequemer ist, sich endlich damit zu befassen. Dieser Code ist übrigens nicht versehentlich so geschrieben.
let price = 5 let quantity = 2 let total = 0 let target = null let storage = [] function record () { storage.push(target) } function replay () { storage.forEach(run => run()) } target = () => { total = price * quantity } record() target() price = 20 console.log(total)
Dies wird nach dem Start in der Browserkonsole angezeigt.
Code ErgebnisHerausforderung: eine zuverlässige Lösung zum Speichern von Funktionen
Wir können die benötigten Funktionen bei Bedarf weiter aufschreiben, aber es wäre schön, wenn wir eine zuverlässigere Lösung hätten, die mit der Anwendung skaliert werden kann. Vielleicht ist es eine Klasse, die eine Liste von Funktionen verwaltet, die ursprünglich in die Zielvariable geschrieben wurden, und die Benachrichtigungen erhält, wenn wir diese Funktionen erneut ausführen müssen.
▍Lösung: Abhängigkeitsklasse
Ein Ansatz zur Lösung des oben genannten Problems besteht darin, das von uns benötigte Verhalten in einer Klasse zu kapseln, die als Abhängigkeit bezeichnet werden kann. Diese Klasse implementiert das Standard-Beobachter-Programmiermuster.
Wenn wir eine JS-Klasse erstellen, die zum Verwalten unserer Abhängigkeiten verwendet wird (was der Implementierung ähnlicher Mechanismen in Vue nahe kommt), sieht dies möglicherweise folgendermaßen aus:
class Dep { // Dep - Dependency constructor () { this.subscribers = [] // , // notify() } depend () { // record if (target && !this.subscribers.includes(target)){ // target // this.subscribers.push(target) } } notify () { // replay this.subscribers.forEach(sub => sub()) // - } }
Bitte beachten Sie, dass wir anstelle des
storage
jetzt unsere anonymen Funktionen im
subscribers
speichern. Anstelle der
record
wird jetzt die
depend
Methode aufgerufen. Auch hier wird anstelle der
replay
die
notify
. So führen Sie unseren Code mit der
Dep
Klasse aus:
const dep = new Dep() let price = 5 let quantity = 2 let total = 0 let target = () => { total = price * quantity } dep.depend()
Unser neuer Code funktioniert genauso wie zuvor, aber jetzt ist er besser gestaltet und fühlt sich besser für die Wiederverwendung an.
Das einzige, was bisher seltsam erscheint, ist die Arbeit mit einer in der
target
gespeicherten Funktion.
Aufgabe: Mechanismus zum Erstellen anonymer Funktionen
In Zukunft müssen wir für jede Variable ein Objekt der Klasse
Dep
erstellen. Darüber hinaus wäre es schön, das Verhalten beim Erstellen anonymer Funktionen irgendwo zusammenzufassen, die beim Aktualisieren der relevanten Daten aufgerufen werden sollten. Vielleicht hilft uns dies bei einer zusätzlichen Funktion, die wir als
watcher
. Dies führt dazu, dass wir diese Konstruktion aus dem vorherigen Beispiel durch eine neue Funktion ersetzen können:
let target = () => { total = price * quantity } dep.depend() target()
Tatsächlich sieht ein Aufruf der
watcher
Funktion, die diesen Code ersetzt, folgendermaßen aus:
watcher(() => { total = price * quantity })
▍ Lösung: Überwachungsfunktion
Innerhalb der
watcher
Funktion, deren Code unten dargestellt ist, können wir mehrere einfache Aktionen ausführen:
function watcher(myFunc) { target = myFunc // target myFunc dep.depend() // target target() // target = null // target }
Wie Sie sehen können, verwendet die
watcher
Funktion als Argument die
myFunc
Funktion, schreibt sie in die globale Zielvariable, ruft
dep.depend()
auf, um diese Funktion zur Liste der Abonnenten hinzuzufügen, ruft diese Funktion auf und setzt die Zielvariable zurück.
Jetzt erhalten wir alle die gleichen Werte 10 und 40, wenn wir den folgenden Code ausführen:
price = 20 console.log(total) dep.notify() console.log(total)
Vielleicht fragen Sie sich, warum wir
target
als globale Variable implementiert haben, anstatt diese Variable bei Bedarf an unsere Funktionen zu übergeben. Wir haben gute Gründe, genau das zu tun, später werden Sie verstehen.
Aufgabe: eigenes Dep-Objekt für jede Variable
Wir haben ein einzelnes Objekt der Klasse
Dep
. Was ist, wenn jede unserer Variablen ein eigenes
Dep
Klassenobjekt haben muss? Bevor wir fortfahren, verschieben wir die Daten, mit denen wir arbeiten, in die Eigenschaften des Objekts:
let data = { price: 5, quantity: 2 }
Stellen Sie sich für einen Moment vor, dass jede unserer Eigenschaften (
price
und
quantity
) ein eigenes internes
Dep
Klassenobjekt hat.
Preis- und MengeneigenschaftenJetzt können wir die
watcher
Funktion folgendermaßen aufrufen:
watcher(() => { total = data.price * data.quantity })
Da wir hier mit dem Wert der Eigenschaft
data.price
, benötigen wir das
Dep
Klassenobjekt der Eigenschaft
price
, um eine anonyme Funktion (im
target
gespeichert) in ihrem Abonnentenarray zu platzieren (durch Aufrufen von
dep.depend()
). Da wir hier mit
data.quantity
, benötigen wir außerdem das
Dep
Objekt der
quantity
Eigenschaft, um eine anonyme Funktion (wieder im
target
gespeichert) in ihrem Abonnenten-Array zu platzieren.
Wenn Sie dies in Form eines Diagramms darstellen, erhalten Sie Folgendes.
Funktionen fallen in Abonnentenarrays von Dep-Klassenobjekten, die unterschiedlichen Eigenschaften entsprechenWenn wir eine weitere anonyme Funktion haben, in der wir nur mit der Eigenschaft
data.price
, sollte die entsprechende anonyme Funktion nur in das Depot des Objekts dieser Eigenschaft gehen.
Zusätzliche Beobachter können nur zu einer der verfügbaren Eigenschaften hinzugefügt werden.Wann müssen Sie möglicherweise
dep.notify()
für Funktionen aufrufen, die Änderungen an der
price
Eigenschaft abonniert haben? Dies wird bei
price
. Dies bedeutet, dass der folgende Code für uns funktionieren sollte, wenn unser Beispiel vollständig fertig ist.
Wenn Sie den Preis ändern, müssen Sie hier dep.notify () für alle Funktionen aufrufen, die den Preis ändernDamit alles auf diese Weise funktioniert, benötigen wir eine Möglichkeit, Ereignisse beim Zugriff auf Immobilien abzufangen (in unserem Fall handelt es sich um
price
oder
quantity
). Auf diese Weise können Sie in diesem Fall die
target
in einem Array von Abonnenten speichern und bei Änderung der entsprechenden Variablen die in diesem Array gespeicherte Funktion ausführen.
▍Lösung: Object.defineProperty ()
Jetzt müssen wir uns mit der Standard-ES5-Methode
Object.defineProperty () vertraut machen. Sie können den Eigenschaften von Objekten Getter und Setter zuweisen. Lassen Sie mich, bevor wir zu ihrer praktischen Anwendung übergehen, die Funktionsweise dieser Mechanismen anhand eines einfachen Beispiels demonstrieren.
let data = { price: 5, quantity: 2 } Object.defineProperty(data, 'price', { // price get() { // console.log(`I was accessed`) }, set(newVal) { // console.log(`I was changed`) } }) data.price // data.price = 20 //
Wenn Sie diesen Code in der Browserkonsole ausführen, wird der folgende Text angezeigt.
Getter und Setter ErgebnisseWie Sie sehen können, druckt unser Beispiel einfach ein paar Textzeilen auf die Konsole. Es werden jedoch keine Werte gelesen oder festgelegt, da wir die Standardfunktionalität von Gettern und Setzern neu definiert haben. Wir werden die Funktionalität dieser Methoden wiederherstellen. Es wird nämlich erwartet, dass Getter die Werte der entsprechenden Methoden zurückgeben und Setter sie setzen. Daher fügen wir dem Code eine neue Variable,
internalValue
, hinzu, mit der wir den aktuellen
price
speichern.
let data = { price: 5, quantity: 2 } let internalValue = data.price // Object.defineProperty(data, 'price', { // price get() { // console.log(`Getting price: ${internalValue}`) return internalValue }, set(newVal) { console.log(`Setting price to: ${newVal}`) internalValue = newVal } }) total = data.price * data.quantity // data.price = 20 //
Nun, da Getter und Setter so arbeiten, wie sie funktionieren sollten, was wird Ihrer Meinung nach in die Konsole gelangen, wenn dieser Code ausgeführt wird? Schauen Sie sich die folgende Abbildung an.
Datenausgabe an die KonsoleJetzt haben wir einen Mechanismus, mit dem Sie Benachrichtigungen erhalten können, wenn Sie Eigenschaftswerte lesen und wenn neue Werte in sie geschrieben werden. Nachdem wir den Code ein wenig überarbeitet haben, können wir Getter und Setter mit allen Eigenschaften des
data
ausstatten. Hier verwenden wir die
Object.keys()
-Methode, die ein Array von Schlüsseln des an sie übergebenen Objekts zurückgibt.
let data = { price: 5, quantity: 2 } Object.keys(data).forEach(key => { // data let internalValue = data[key] Object.defineProperty(data, key, { get() { console.log(`Getting ${key}: ${internalValue}`) return internalValue }, set(newVal) { console.log(`Setting ${key} to: ${newVal}`) internalValue = newVal } }) }) let total = data.price * data.quantity data.price = 20
Jetzt haben alle Eigenschaften des
data
Getter und Setter. Dies wird in der Konsole angezeigt, nachdem dieser Code ausgeführt wurde.
Datenausgabe an die Konsole durch Getter und SetterMontage des Reaktivitätssystems
Wenn ein Codefragment wie
total = data.price * data.quantity
und der Wert der
total = data.price * data.quantity
die entsprechende anonyme Funktion (in unserem Fall das Ziel) "merken". Wenn die
price
geändert, dh auf einen neuen Wert gesetzt wird, wird diese Funktion aufgerufen, um die von ihr ausgeführten Vorgänge zu wiederholen, da bekannt ist, dass eine bestimmte Codezeile davon abhängt. Infolgedessen können die in Gettern und Setzern ausgeführten Operationen wie folgt vorgestellt werden:
- Getter - Sie müssen sich an die anonyme Funktion erinnern, die wir erneut aufrufen, wenn sich der Wert ändert.
- Setter - Die gespeicherte anonyme Funktion muss ausgeführt werden, was zu einer Änderung des entsprechenden resultierenden Werts führt.
Wenn Sie die
Dep
Klasse verwenden, die Ihnen in dieser Beschreibung bereits bekannt ist, erhalten Sie Folgendes:
- Beim Lesen eines Eigenschaftswerts wird
dep.depend()
aufgerufen, um die aktuelle target
zu speichern. - Wenn ein Wert in eine Eigenschaft geschrieben wird, wird
dep.notify()
aufgerufen, um alle gespeicherten Funktionen neu zu starten.
Jetzt werden wir diese beiden Ideen kombinieren und schließlich zu dem Code kommen, mit dem wir unser Ziel erreichen können.
let data = { price: 5, quantity: 2 } let target = null // - , class Dep { constructor () { this.subscribers = [] } depend () { if (target && !this.subscribers.includes(target)){ this.subscribers.push(target) } } notify () { this.subscribers.forEach(sub => sub()) } } // , // Object.keys(data).forEach(key => { let internalValue = data[key] // // Dep const dep = new Dep() Object.defineProperty(data, key, { get() { dep.depend() // target return internalValue }, set(newVal) { internalValue = newVal dep.notify() // } }) }) // watcher dep.depend(), // function watcher(myFunc){ target = myFunc target() target = null } watcher(() => { data.total = data.price * data.quantity })
Experimentieren wir mit diesem Code in der Browserkonsole.
Bereit Code-ExperimenteWie Sie sehen, funktioniert es genau so, wie wir es brauchen! Die
price
und
quantity
sind reaktiv geworden! Der gesamte Code, der für die Generierung der
total
wenn sich
price
oder
quantity
ändern, wird wiederholt ausgeführt.
Nachdem wir unser eigenes Reaktivitätssystem geschrieben haben, erscheint Ihnen diese Abbildung aus der Vue-Dokumentation vertraut und verständlich.
Vue-ReaktivitätssystemSehen Sie diesen schönen lila Kreis, der
sagt, die Getter und Setter enthalten? Jetzt sollte er dir vertraut sein. Jede Instanz der Komponente verfügt über eine Instanz der Beobachtermethode (blauer Kreis), die Abhängigkeiten von Gettern sammelt (rote Linie). Wenn der Setter später aufgerufen wird, benachrichtigt er die Beobachtermethode, was zum erneuten Rendern der Komponente führt. Hier ist das gleiche Schema mit Erklärungen, die es mit unserer Entwicklung verbinden.
Vue-Reaktivitätsdiagramm mit ErläuterungenWir glauben, dass dieses Schema, nachdem wir unser eigenes Reaktivitätssystem geschrieben haben, keine zusätzlichen Erklärungen benötigt.
Natürlich ist dies in Vue komplizierter, aber jetzt sollten Sie den Mechanismus verstehen, der Reaktivitätssystemen zugrunde liegt.
Zusammenfassung
Nachdem Sie dieses Material gelesen haben, haben Sie Folgendes gelernt:
- So erstellen Sie eine
Dep
Klasse, die Funktionen mithilfe der depend
Methode sammelt und sie bei Bedarf mithilfe der notify
Methode erneut aufruft. - So erstellen Sie eine Überwachungsfunktion, mit der Sie den von uns ausgeführten Code steuern können (dies ist die
target
), den Sie möglicherweise im Objekt der Dep
Klasse speichern müssen. - Verwendung der
Object.defineProperty()
-Methode zum Erstellen von Get- und Setter-Methoden.
All dies, zusammengefasst in einem einzigen Beispiel, führte zur Schaffung eines Reaktionssystems in reinem JavaScript, indem Sie verstehen, welche Funktionsmerkmale solcher Systeme in modernen Webframeworks verwendet werden können.
Liebe Leser! Wenn Sie sich vor dem Lesen dieses Materials die Merkmale der Mechanismen von Reaktivitätssystemen schlecht vorgestellt haben, sagen Sie mir, haben Sie es jetzt geschafft, mit ihnen umzugehen?