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 priceauf der Webseite.
- Berechnen Sie den Ausdruck, in dem der pricemit derquantitymultipliziert wird, neu und zeigen Sie den resultierenden Wert auf der Seite an.
- Rufen Sie die Funktion totalPriceWithTaxund 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 aktuelletargetzu 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 DepKlasse, die Funktionen mithilfe derdependMethode sammelt und sie bei Bedarf mithilfe dernotifyMethode 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 derDepKlasse 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?