On-Demand-Reaktivität in Vue 3

Veröffentlicht: 2022-03-11

Abgesehen von bewundernswerten Leistungsverbesserungen brachte das kürzlich veröffentlichte Vue 3 auch einige neue Funktionen. Die wohl wichtigste Einführung ist die Composition API . Im ersten Teil dieses Artikels fassen wir die Standardmotivation für eine neue API zusammen: bessere Organisation und Wiederverwendung von Code. Im zweiten Teil werden wir uns auf weniger diskutierte Aspekte der Verwendung der neuen API konzentrieren, wie z. B. die Implementierung reaktivitätsbasierter Funktionen, die im Reaktivitätssystem von Vue 2 nicht ausdrückbar waren.

Wir werden dies als On-Demand-Reaktivität bezeichnen. Nach der Einführung der relevanten neuen Funktionen werden wir eine einfache Tabellenkalkulationsanwendung erstellen, um die neue Ausdruckskraft des Reaktivitätssystems von Vue zu demonstrieren. Ganz am Ende werden wir diskutieren, welchen praktischen Nutzen diese Verbesserung der On-Demand-Reaktivität haben könnte.

Was ist neu in Vue 3 und warum es wichtig ist

Vue 3 ist eine umfassende Neufassung von Vue 2, die eine Fülle von Verbesserungen einführt, während die Abwärtskompatibilität mit der alten API fast vollständig erhalten bleibt.

Eine der wichtigsten neuen Funktionen in Vue 3 ist die Composition API . Ihre Einführung löste viele Kontroversen aus, als sie erstmals öffentlich diskutiert wurde. Falls Sie mit der neuen API noch nicht vertraut sind, beschreiben wir zunächst die Motivation dahinter.

Die übliche Einheit der Codeorganisation ist ein JavaScript-Objekt, dessen Schlüssel verschiedene mögliche Typen eines Teils einer Komponente darstellen. So könnte das Objekt einen Abschnitt für reaktive Daten ( data ), einen anderen Abschnitt für berechnete Eigenschaften ( computed ), einen weiteren für Komponentenmethoden ( methods ) usw. haben.

Unter diesem Paradigma kann eine Komponente mehrere nicht verwandte oder lose verwandte Funktionalitäten haben, deren innere Abläufe auf die oben erwähnten Komponentenabschnitte verteilt sind. Beispielsweise könnten wir eine Komponente zum Hochladen von Dateien haben, die zwei im Wesentlichen getrennte Funktionalitäten implementiert: Dateiverwaltung und ein System, das die Animation des Upload-Status steuert.

Der <script> -Teil könnte etwa Folgendes enthalten:

 export default { data () { return { animation_state: 'playing', animation_duration: 10, upload_filenames: [], upload_params: { target_directory: 'media', visibility: 'private', } } }, computed: { long_animation () { return this.animation_duration > 5; }, upload_requested () { return this.upload_filenames.length > 0; }, }, ... }

Dieser traditionelle Ansatz zur Codeorganisation hat Vorteile, hauptsächlich darin, dass sich der Entwickler keine Gedanken darüber machen muss, wo er einen neuen Code schreiben soll. Wenn wir eine reaktive Variable hinzufügen, fügen wir sie in den data ein. Wenn wir nach einer vorhandenen Variablen suchen, wissen wir, dass sie sich im data befinden muss.

Dieser traditionelle Ansatz, die Implementierung der Funktionalität in Abschnitte ( data , computed usw.) aufzuteilen, ist nicht in allen Situationen geeignet.

Folgende Ausnahmen werden häufig genannt:

  1. Umgang mit einer Komponente mit vielen Funktionalitäten. Wenn wir unseren Animationscode beispielsweise um die Möglichkeit erweitern möchten, den Start der Animation zu verzögern, müssen wir in einem Code-Editor zwischen allen relevanten Abschnitten der Komponente scrollen/springen. Im Fall unserer Komponente zum Hochladen von Dateien ist die Komponente selbst klein und die Anzahl der von ihr implementierten Funktionalitäten ist ebenfalls klein. In diesem Fall ist das Springen zwischen den Abschnitten also kein wirkliches Problem. Dieses Problem der Codefragmentierung wird relevant, wenn wir uns mit großen Komponenten befassen.
  2. Eine andere Situation, in der der traditionelle Ansatz fehlt, ist die Wiederverwendung von Code. Oft müssen wir eine bestimmte Kombination aus reaktiven Daten, berechneten Eigenschaften, Methoden usw. in mehr als einer Komponente verfügbar machen.

Vue 2 (und das abwärtskompatible Vue 3) bieten eine Lösung für die meisten Probleme bei der Codeorganisation und Wiederverwendung: Mixins .

Vor- und Nachteile von Mixins in Vue 3

Mixins ermöglichen es, die Funktionalitäten einer Komponente in einer separaten Codeeinheit zu extrahieren. Jede Funktionalität wird in ein separates Mixin gestellt und jede Komponente kann ein oder mehrere Mixins verwenden. In einem Mixin definierte Teile können in einer Komponente verwendet werden, als ob sie in der Komponente selbst definiert wären. Die Mixins sind ein bisschen wie Klassen in objektorientierten Sprachen, da sie den Code sammeln, der sich auf eine bestimmte Funktionalität bezieht. Wie Klassen können Mixins in anderen Codeeinheiten vererbt (verwendet) werden.

Das Argumentieren mit Mixins ist jedoch schwieriger, da Mixins im Gegensatz zu Klassen nicht mit Blick auf die Kapselung entworfen werden müssen. Mixins dürfen Sammlungen von lose gebundenen Codestücken ohne klar definierte Schnittstelle zur Außenwelt sein. Die gleichzeitige Verwendung von mehr als einem Mixin in derselben Komponente kann zu einer Komponente führen, die schwer zu verstehen und zu verwenden ist.

Die meisten objektorientierten Sprachen (z. B. C# und Java) raten von Mehrfachvererbung ab oder verbieten sie sogar, obwohl das objektorientierte Programmierparadigma über die Werkzeuge verfügt, um mit einer solchen Komplexität umzugehen. (Einige Sprachen erlauben Mehrfachvererbung, wie C++, aber die Komposition wird immer noch der Vererbung vorgezogen.)

Ein praktischeres Problem, das bei der Verwendung von Mixins in Vue auftreten kann, ist die Namenskollision, die auftritt, wenn zwei oder mehr Mixins verwendet werden, die allgemeine Namen deklarieren. An dieser Stelle sei angemerkt, dass die Strategie vom Entwickler angepasst werden kann, wenn die Standardstrategie von Vue für den Umgang mit Namenskollisionen in einer bestimmten Situation nicht ideal ist. Dies geht auf Kosten einer höheren Komplexität.

Ein weiteres Problem ist, dass Mixins nichts Ähnliches wie einen Klassenkonstruktor bieten. Dies ist ein Problem, da wir häufig eine sehr ähnliche, aber nicht exakt gleiche Funktionalität benötigen, die in verschiedenen Komponenten vorhanden sein muss. Dies kann in einigen einfachen Fällen durch die Verwendung von Mixin-Factorys umgangen werden.

Daher sind Mixins nicht die ideale Lösung für die Organisation und Wiederverwendung von Code, und je größer das Projekt, desto schwerwiegender werden ihre Probleme. Vue 3 führt einen neuen Weg ein, um die gleichen Probleme in Bezug auf die Organisation und Wiederverwendung von Code zu lösen.

Kompositions-API: Die Antwort von Vue 3 auf die Organisation und Wiederverwendung von Code

Die Kompositions-API ermöglicht uns (aber erfordert uns nicht ), die Teile einer Komponente vollständig zu entkoppeln. Jedes Stück Code – eine Variable, eine berechnete Eigenschaft, eine Uhr usw. – kann unabhängig definiert werden.

Anstatt beispielsweise ein Objekt zu haben, das einen data enthält, der einen Schlüssel animation_state mit dem (Standard-)Wert „playing“ enthält, können wir jetzt schreiben (irgendwo in unserem JavaScript-Code):

 const animation_state = ref('playing');

Der Effekt ist fast derselbe wie die Deklaration dieser Variablen im data einer Komponente. Der einzige wesentliche Unterschied besteht darin, dass wir die außerhalb der ref definierte Referenz in der Komponente verfügbar machen müssen, in der wir sie verwenden möchten. Wir tun dies, indem wir sein Modul an den Ort importieren, an dem die Komponente definiert ist, und die ref aus dem setup -Abschnitt einer Komponente zurückgeben. Wir überspringen dieses Verfahren vorerst und konzentrieren uns für einen Moment nur auf die neue API. Reaktivität in Vue 3 erfordert keine Komponente; es ist eigentlich ein in sich geschlossenes System.

Wir können die Variable animation_state in jedem Bereich verwenden, in den wir diese Variable importieren. Nachdem wir eine ref erstellt haben, erhalten und setzen wir ihren tatsächlichen Wert mit ref.value , zum Beispiel:

 animation_state.value = 'paused'; console.log(animation_state.value);

Wir benötigen das Suffix „.value“, da der Zuweisungsoperator sonst der Variable animation_state den (nicht reaktiven) Wert „paused“ zuweisen würde. Reaktivität in JavaScript (sowohl wenn es durch die defineProperty wie in Vue 2 implementiert wird, als auch wenn es auf einem Proxy basiert wie in Vue 3) erfordert ein Objekt, mit dessen Schlüsseln wir reaktiv arbeiten können.

Beachten Sie, dass dies auch in Vue 2 der Fall war; Dort hatten wir eine Komponente als Präfix für jedes reaktive Datenelement ( component.data_member ). Solange der JavaScript-Sprachstandard nicht die Möglichkeit einführt, den Zuweisungsoperator zu überladen, erfordern reaktive Ausdrücke ein Objekt und einen Schlüssel (z. B. animation_state und value wie oben), um auf der linken Seite jeder Zuweisungsoperation zu erscheinen, wo wir dies wünschen Reaktivität bewahren.

In Vorlagen können wir .value weglassen, da Vue den Vorlagencode vorverarbeiten muss und Verweise automatisch erkennen kann:

 <animation :state='animation_state' />

Theoretisch könnte der Vue-Compiler auch den <script> -Teil einer Single File Component (SFC) auf ähnliche Weise vorverarbeiten und bei Bedarf .value . Die Verwendung von refs würde sich dann jedoch unterscheiden, je nachdem, ob wir SFCs verwenden oder nicht, sodass eine solche Funktion möglicherweise nicht einmal wünschenswert ist.

Manchmal haben wir eine Entität (z. B. ein Javascript-Objekt oder ein Array), die wir niemals durch eine völlig andere Instanz ersetzen möchten. Stattdessen könnten wir nur daran interessiert sein, die Schlüsselfelder zu ändern. In diesem Fall gibt es eine Abkürzung: Durch die Verwendung von „ reactive “ anstelle von „ ref “ können wir auf den .value verzichten:

 const upload_params = reactive({ target_directory: 'media', visibility: 'private', }); upload_params.visibility = 'public'; // no `.value` needed here // if we did not make `upload_params` constant, the following code would compile but we would lose reactivity after the assignment; it is thus a good idea to make reactive variables ```const``` explicitly: upload_params = { target_directory: 'static', visibility: 'public', };

Die entkoppelte Reaktivität mit ref und reactive ist kein völlig neues Feature von Vue 3. Sie wurde teilweise in Vue 2.6 eingeführt, wo solche entkoppelten Instanzen reaktiver Daten als „Observables“ bezeichnet wurden. Zum größten Teil kann man Vue.observable durch reactive ersetzen. Einer der Unterschiede besteht darin, dass der Zugriff auf und die Mutation des Objekts, das direkt an Vue.observable wird, reaktiv ist, während die neue API ein Proxy-Objekt zurückgibt, sodass die Mutation des ursprünglichen Objekts keine reaktiven Auswirkungen hat.

Vergleich: Options-API vs. Kompositions-API.

Ganz neu in Vue 3 ist, dass neben reaktiven Daten nun auch andere reaktive Teile eines Bauteils eigenständig definiert werden können. Berechnete Eigenschaften werden wie erwartet implementiert:

 const x = ref(5); const x_squared = computed(() => x.value * x.value); console.log(x_squared.value); // outputs 25

Ebenso können verschiedene Arten von Überwachungen, Lebenszyklusmethoden und Abhängigkeitsinjektion implementiert werden. Der Kürze halber werden wir diese hier nicht behandeln.

Angenommen, wir verwenden den Standard-SFC-Ansatz für die Vue-Entwicklung. Möglicherweise verwenden wir sogar die herkömmliche API mit separaten Abschnitten für Daten, berechnete Eigenschaften usw. Wie integrieren wir die kleine Reaktivität der Kompositions-API in SFCs? Vue 3 führt dafür einen weiteren Abschnitt ein: setup . Der Abschnitt new kann als neue Lebenszyklusmethode betrachtet werden (die vor allen anderen Hooks ausgeführt wird – insbesondere vor created ).

Hier ist ein Beispiel für eine vollständige Komponente, die den traditionellen Ansatz mit der Composition API integriert:

 <template> <input v-model="x" /> <div>Squared: {{ x_squared }}, negative: {{ x_negative }}</div> </template> <script> import { ref, computed } from 'vue'; export default { name: "Demo", computed: { x_negative() { return -this.x; } }, setup() { const x = ref(0); const x_squared = computed(() => x.value * x.value); return {x, x_squared}; } } </script>

Dinge, die Sie aus diesem Beispiel mitnehmen sollten:

  • Der gesamte Kompositions-API-Code befindet sich jetzt in setup . Möglicherweise möchten Sie für jede Funktionalität eine separate Datei erstellen, diese Datei in ein SFC importieren und die gewünschten Reaktivitätsbits aus dem setup zurückgeben (um sie für den Rest der Komponente verfügbar zu machen).
  • Sie können den neuen und den traditionellen Ansatz in derselben Datei mischen. Beachten Sie, dass x , obwohl es sich um eine Referenz handelt, kein .value erfordert, wenn im Vorlagencode oder in herkömmlichen Abschnitten einer Komponente wie beispielsweise computed darauf verwiesen wird.
  • Beachten Sie zu guter Letzt, dass wir zwei Root-DOM-Knoten in unserer Vorlage haben. Die Möglichkeit, mehrere Root-Knoten zu haben, ist ein weiteres neues Feature von Vue 3.

Die Reaktivität ist in Vue 3 ausdrucksstärker

Im ersten Teil dieses Artikels haben wir die Standardmotivation für die Composition API angesprochen, nämlich eine verbesserte Codeorganisation und -wiederverwendung. Tatsächlich ist das Hauptverkaufsargument der neuen API nicht ihre Leistungsfähigkeit, sondern der organisatorische Komfort, den sie mit sich bringt: die Möglichkeit, den Code klarer zu strukturieren. Es scheint, als wäre das alles – dass die Composition API eine Möglichkeit bietet, Komponenten zu implementieren, die die Einschränkungen der bereits bestehenden Lösungen wie Mixins vermeidet.

Die neue API bietet jedoch noch mehr. Die Composition API ermöglicht tatsächlich nicht nur besser organisierte, sondern leistungsfähigere reaktive Systeme. Der Hauptbestandteil ist die Fähigkeit, der Anwendung dynamisch Reaktivität hinzuzufügen. Früher musste man vor dem Laden eines Bauteils alle Daten, alle berechneten Eigenschaften usw. definieren. Warum wäre das nachträgliche Hinzufügen von reaktiven Objekten sinnvoll? Im Folgenden betrachten wir ein komplexeres Beispiel: Tabellenkalkulationen.

Erstellen einer Tabelle in Vue 2

Tabellenkalkulationstools wie Microsoft Excel, LibreOffice Calc und Google Sheets haben alle eine Art Reaktivitätssystem. Diese Tools präsentieren einem Benutzer eine Tabelle mit Spalten, die mit A–Z, AA–ZZ, AAA–ZZZ usw. indiziert sind, und Zeilen, die numerisch indiziert sind.

Jede Zelle kann einen einfachen Wert oder eine Formel enthalten. Eine Zelle mit einer Formel ist im Wesentlichen eine berechnete Eigenschaft, die von Werten oder anderen berechneten Eigenschaften abhängen kann. Mit Standardtabellen (und im Gegensatz zum Reaktivitätssystem in Vue) dürfen diese berechneten Eigenschaften sogar von sich selbst abhängen! Eine solche Selbstreferenz ist in einigen Szenarios nützlich, in denen der gewünschte Wert durch iterative Annäherung erhalten wird.

Sobald sich der Inhalt einer Zelle ändert, lösen alle Zellen, die von der betreffenden Zelle abhängen, eine Aktualisierung aus. Wenn weitere Änderungen auftreten, können weitere Updates geplant werden.

Wenn wir mit Vue eine Tabellenkalkulationsanwendung erstellen würden, wäre es natürlich zu fragen, ob wir das Vue-eigene Reaktivitätssystem verwenden und Vue zur Engine einer Tabellenkalkulationsanwendung machen können. Für jede Zelle konnten wir uns ihren bearbeitbaren Rohwert sowie den entsprechenden berechneten Wert merken. Berechnete Werte würden den Rohwert widerspiegeln, wenn es sich um einen einfachen Wert handelt, und andernfalls sind die berechneten Werte das Ergebnis des Ausdrucks (der Formel), der anstelle eines einfachen Werts geschrieben wird.

Mit Vue 2 können Sie eine Tabellenkalkulation implementieren, raw_values ​​ein zweidimensionales Array von Zeichenfolgen und computed_values ​​ein (berechnetes) zweidimensionales Array von Zellenwerten haben.

Wenn die Anzahl der Zellen klein und fest ist, bevor die entsprechende Vue-Komponente geladen wird, könnten wir einen Rohwert und einen berechneten Wert für jede Zelle der Tabelle in unserer Komponentendefinition haben. Abgesehen von der ästhetischen Ungeheuerlichkeit, die eine solche Implementierung verursachen würde, zählt eine Tabelle mit einer festen Anzahl von Zellen zur Kompilierzeit wahrscheinlich nicht als Tabellenkalkulation.

Es gibt auch Probleme mit dem zweidimensionalen Array computed_values ​​. Eine berechnete Eigenschaft ist immer eine Funktion, deren Bewertung in diesem Fall von ihr selbst abhängt (um den Wert einer Zelle zu berechnen, müssen im Allgemeinen einige andere Werte bereits berechnet werden). Selbst wenn Vue selbstreferenzielle berechnete Eigenschaften zulässt, würde das Aktualisieren einer einzelnen Zelle dazu führen, dass alle Zellen neu berechnet werden (unabhängig davon, ob es Abhängigkeiten gibt oder nicht). Dies wäre äußerst ineffizient. Daher könnten wir am Ende Reaktivität verwenden, um Änderungen in den Rohdaten mit Vue 2 zu erkennen, aber alles andere in Bezug auf Reaktivität müsste von Grund auf neu implementiert werden.

Modellierung berechneter Werte in Vue 3

Mit Vue 3 können wir für jede Zelle eine neue berechnete Eigenschaft einführen. Wenn die Tabelle wächst, werden neue berechnete Eigenschaften eingeführt.

Angenommen, wir haben die Zellen A1 und A2 und möchten, dass A2 das Quadrat von A1 anzeigt, dessen Wert die Zahl 5 ist. Eine Skizze dieser Situation:

 let A1 = computed(() => 5); let A2 = computed(() => A1.value * A1.value); console.log(A2.value); // outputs 25

Angenommen, wir bleiben einen Moment in diesem einfachen Szenario. Hier liegt ein Problem vor; Was ist, wenn wir A1 so ändern möchten, dass es die Zahl 6 enthält? Angenommen, wir schreiben dies:

 A1 = computed(() => 6); console.log(A2.value); // outputs 25 if we already ran the code above

Dadurch wurde nicht nur der Wert 5 in A1 auf 6 geändert. Die Variable A1 hat jetzt eine völlig andere Identität: die berechnete Eigenschaft, die sich in die Zahl 6 auflöst. Die Variable A2 reagiert jedoch immer noch auf Änderungen der alten Identität der Variablen A1 . A2 sollte sich also nicht direkt auf A1 beziehen, sondern auf ein spezielles Objekt, das immer im Kontext verfügbar sein wird und uns mitteilt, was A1 im Moment ist. Mit anderen Worten, wir brauchen eine Indirektionsebene, bevor wir auf A1 zugreifen, so etwas wie einen Zeiger. In Javascript gibt es keine Zeiger als erstklassige Entitäten, aber es ist einfach, einen zu simulieren. Wenn wir einen pointer haben möchten, der auf einen value zeigt, können wir einen Objektzeiger pointer = {points_to: value} erstellen. Das Umleiten des Zeigers entspricht der Zuweisung an pointer.points_to , und die Dereferenzierung (Zugriff auf den Wert, auf den gezeigt wird) entspricht dem Abrufen des Werts von pointer.points_to . In unserem Fall gehen wir wie folgt vor:

 let A1 = reactive({points_to: computed(() => 5)}); let A2 = reactive({points_to: computed(() => A1.points_to * A1.points_to)}); console.log(A2.points_to); // outputs 25

Jetzt können wir 5 durch 6 ersetzen.

 A1.points_to = computed(() => 6); console.log(A2.points_to); // outputs 36

Auf dem Discord-Server von Vue schlug der Benutzer redblobgames einen anderen interessanten Ansatz vor: Verwenden Sie anstelle von berechneten Werten Referenzen, die reguläre Funktionen umschließen. Auf diese Weise kann man die Funktion auf ähnliche Weise austauschen, ohne die Identität der Referenz selbst zu ändern.

Unsere Tabellenkalkulationsimplementierung wird Zellen haben, auf die durch Schlüssel eines zweidimensionalen Arrays verwiesen wird. Dieses Array kann die von uns benötigte Indirektionsebene bereitstellen. In unserem Fall benötigen wir also keine zusätzliche Zeigersimulation. Wir könnten sogar ein Array haben, das nicht zwischen Roh- und berechneten Werten unterscheidet. Alles kann ein berechneter Wert sein:

 const cells = reactive([ computed(() => 5), computed(() => cells[0].value * cells[0].value) ]); cells[0] = computed(() => 6); console.log(cells[1].value); // outputs 36

Wir möchten jedoch Rohwerte und berechnete Werte wirklich unterscheiden, da wir in der Lage sein möchten, den Rohwert an ein HTML-Eingabeelement zu binden. Wenn wir außerdem ein separates Array für Rohwerte haben, müssen wir die Definitionen der berechneten Eigenschaften nie ändern; sie werden automatisch basierend auf den Rohdaten aktualisiert.

Implementierung der Tabellenkalkulation

Beginnen wir mit einigen grundlegenden Definitionen, die größtenteils selbsterklärend sind.

 const rows = ref(30), cols = ref(26); /* if a string codes a number, return the number, else return a string */ const as_number = raw_cell => /^[0-9]+(\.[0-9]+)?$/.test(raw_cell) ? Number.parseFloat(raw_cell) : raw_cell; const make_table = (val = '', _rows = rows.value, _cols = cols.value) => Array(_rows).fill(null).map(() => Array(_cols).fill(val)); const raw_values = reactive(make_table('', rows.value, cols.value)); const computed_values = reactive(make_table(undefined, rows.value, cols.value)); /* a useful metric for debugging: how many times did cell (re)computations occur? */ const calculations = ref(0);

Der Plan sieht vor, dass alle computed_values[row][column] wie folgt berechnet werden. Wenn raw_values[row][column] nicht mit = beginnt, geben raw_values[row][column] zurück. Analysieren Sie andernfalls die Formel, kompilieren Sie sie in JavaScript, werten Sie den kompilierten Code aus und geben Sie den Wert zurück. Um es kurz zu machen, schummeln wir ein wenig mit Parsing-Formeln und verzichten hier auf einige offensichtliche Optimierungen, wie etwa einen Compilation-Cache.

Wir gehen davon aus, dass Benutzer jeden gültigen JavaScript-Ausdruck als Formel eingeben können. Wir können Verweise auf Zellnamen, die in den Ausdrücken des Benutzers erscheinen, wie A1, B5 usw., durch den Verweis auf den tatsächlichen Zellwert (berechnet) ersetzen. Die folgende Funktion erledigt diese Aufgabe unter der Annahme, dass Zeichenfolgen, die Zellnamen ähneln, wirklich immer Zellen identifizieren (und nicht Teil eines nicht verwandten JavaScript-Ausdrucks sind). Der Einfachheit halber gehen wir davon aus, dass Spaltenindizes aus einem einzigen Buchstaben bestehen.

 const letters = Array(26).fill(0) .map((_, i) => String.fromCharCode("A".charCodeAt(0) + i)); const transpile = str => { let cell_replacer = (match, prepend, col, row) => { col = letters.indexOf(col); row = Number.parseInt(row) - 1; return prepend + ` computed_values[${row}][${col}].value `; }; return str.replace(/(^|[^AZ])([AZ])([0-9]+)/g, cell_replacer); };

Mit der transpile Funktion können wir aus Ausdrücken, die in unserer kleinen „Erweiterung“ von JavaScript mit Zellbezügen geschrieben wurden, reine JavaScript-Ausdrücke erhalten.

Der nächste Schritt besteht darin, berechnete Eigenschaften für jede Zelle zu generieren. Dieser Vorgang findet einmal im Leben jeder Zelle statt. Wir können eine Fabrik erstellen, die die gewünschten berechneten Eigenschaften zurückgibt:

 const computed_cell_generator = (i, j) => { const computed_cell = computed(() => { // we don't want Vue to think that the value of a computed_cell depends on the value of `calculations` nextTick(() => ++calculations.value); let raw_cell = raw_values[i][j].trim(); if (!raw_cell || raw_cell[0] != '=') return as_number(raw_cell); let user_code = raw_cell.substring(1); let code = transpile(user_code); try { // the constructor of a Function receives the body of a function as a string let fn = new Function(['computed_values'], `return ${code};`); return fn(computed_values); } catch (e) { return "ERROR"; } }); return computed_cell; }; for (let i = 0; i < rows.value; ++i) for (let j = 0; j < cols.value; ++j) computed_values[i][j] = computed_cell_generator(i, j);

Wenn wir den gesamten obigen Code in die setup -Methode einfügen, müssen wir {raw_values, computed_values, rows, cols, letters, calculations} .

Im Folgenden präsentieren wir die vollständige Komponente zusammen mit einer grundlegenden Benutzeroberfläche.

Der Code ist auf GitHub verfügbar, und Sie können sich auch die Live-Demo ansehen.

 <template> <div> <div>Calculations: {{ calculations }}</div> <table class="table" border="0"> <tr class="row"> <td></td> <td class="column" v-for="(_, j) in cols" :key="'header' + j" > {{ letters[j] }} </td> </tr> <tr class="row" v-for="(_, i) in rows" :key="i" > <td class="column"> {{ i + 1 }} </td> <td class="column" v-for="(__, j) in cols" :key="i + '-' + j" :class="{ column_selected: active(i, j), column_inactive: !active(i, j), }" @click="activate(i, j)" > <div v-if="active(i, j)"> <input :ref="'input' + i + '-' + j" v-model="raw_values[i][j]" @keydown.enter.prevent="ui_enter()" @keydown.esc="ui_esc()" /> </div> <div v-else v-html="computed_value_formatter(computed_values[i][j].value)"/> </td> </tr> </table> </div> </template> <script> import {ref, reactive, computed, watchEffect, toRefs, nextTick, onUpdated} from "vue"; export default { name: 'App', components: {}, data() { return { ui_editing_i: null, ui_editing_j: null, } }, methods: { get_dom_input(i, j) { return this.$refs['input' + i + '-' + j]; }, activate(i, j) { this.ui_editing_i = i; this.ui_editing_j = j; nextTick(() => this.get_dom_input(i, j).focus()); }, active(i, j) { return this.ui_editing_i === i && this.ui_editing_j === j; }, unselect() { this.ui_editing_i = null; this.ui_editing_j = null; }, computed_value_formatter(str) { if (str === undefined || str === null) return 'none'; return str; }, ui_enter() { if (this.ui_editing_i < this.rows - 1) this.activate(this.ui_editing_i + 1, this.ui_editing_j); else this.unselect(); }, ui_esc() { this.unselect(); }, }, setup() { /*** All the code we wrote above goes here. ***/ return {raw_values, computed_values, rows, cols, letters, calculations}; }, } </script> <style> .table { margin-left: auto; margin-right: auto; margin-top: 1ex; border-collapse: collapse; } .column { box-sizing: border-box; border: 1px lightgray solid; } .column:first-child { background: #f6f6f6; min-width: 3em; } .column:not(:first-child) { min-width: 4em; } .row:first-child { background: #f6f6f6; } #empty_first_cell { background: white; } .column_selected { border: 2px cornflowerblue solid !important; padding: 0px; } .column_selected input, .column_selected input:active, .column_selected input:focus { outline: none; border: none; } </style>

Was ist mit der realen Nutzung?

Wir haben gesehen, wie das entkoppelte Reaktivitätssystem von Vue 3 nicht nur saubereren Code ermöglicht, sondern auch komplexere reaktive Systeme, die auf dem neuen Reaktivitätsmechanismus von Vue basieren. Seit der Einführung von Vue sind rund sieben Jahre vergangen, und die Steigerung der Ausdruckskraft war offensichtlich nicht sehr gefragt.

Das Tabellenbeispiel ist eine einfache Demonstration dessen, wozu Vue jetzt in der Lage ist, und Sie können sich auch die Live-Demo ansehen.

Aber als reales Beispiel ist es eine Art Nische. In welchen Situationen könnte das neue System nützlich sein? Der offensichtlichste Anwendungsfall für die On-Demand-Reaktivität könnte in Leistungssteigerungen für komplexe Anwendungen liegen.

Trichtervergleich zwischen Vue 2 und Vue 3.

In Front-End-Anwendungen, die mit einer großen Datenmenge arbeiten, kann sich der Overhead einer schlecht durchdachten Reaktivität negativ auf die Leistung auswirken. Angenommen, wir haben eine Business-Dashboard-Anwendung, die interaktive Berichte über die Geschäftstätigkeit des Unternehmens erstellt. Der Benutzer kann einen Zeitraum auswählen und Leistungsindikatoren im Bericht hinzufügen oder entfernen. Einige Indikatoren können Werte anzeigen, die von anderen Indikatoren abhängen.

Eine Möglichkeit, die Berichterstellung zu implementieren, ist eine monolithische Struktur. Wenn der Benutzer einen Eingabeparameter in der Schnittstelle ändert, wird eine einzelne berechnete Eigenschaft, z. B. report_data , aktualisiert. Die Berechnung dieser berechneten Eigenschaft erfolgt nach einem fest programmierten Plan: Zuerst werden alle unabhängigen Leistungsindikatoren berechnet, dann diejenigen, die nur von diesen unabhängigen Indikatoren abhängen usw.

Eine bessere Implementierung entkoppelt Bits des Berichts und berechnet sie unabhängig voneinander. Dies hat einige Vorteile:

  • Der Entwickler muss keinen Ausführungsplan hartcodieren, was mühsam und fehleranfällig ist. Das Reaktivitätssystem von Vue erkennt automatisch Abhängigkeiten.
  • Je nach Datenmenge können wir erhebliche Leistungssteigerungen erzielen, da wir nur die Berichtsdaten aktualisieren, die logischerweise von den geänderten Eingabeparametern abhängen.

Wenn alle Leistungsindikatoren, die Teil des Abschlussberichts sein können, bekannt sind, bevor die Vue-Komponente geladen wird, können wir die vorgeschlagene Entkopplung möglicherweise sogar mit Vue 2 implementieren. Andernfalls, wenn das Backend die einzige Quelle der Wahrheit ist (was Dies ist normalerweise bei datengesteuerten Anwendungen der Fall) oder wenn es externe Datenanbieter gibt, können wir bei Bedarf berechnete Eigenschaften für jeden Teil eines Berichts generieren.

Dank Vue 3 ist dies jetzt nicht nur möglich, sondern auch einfach möglich.