Können React Hooks stattdessen auf ähnliche Weise wie die Vue 3 Composition API implementiert werden?

Nein, denn es ist ein ganz anderes mentales Modell. Ich denke, dass viele Studenten nur auf die Ähnlichkeiten zwischen den beiden API-Sätzen in Bezug auf die Funktionalität achten und die Unterschiede im „Gesamtbild“ der beiden Frameworks unterschätzen.

Bevor ich mit dem Text beginne, möchte ich das sagen

  1. Erstens geben die in diesem Artikel geäußerten Ansichten nicht das Unternehmen wieder. Ich hatte das Gefühl, dass es im Kreis zu viele Stimmen gab, die mit Hooks nicht einverstanden waren (nichts für ungut), also meldete ich mich freiwillig, herauszukommen und die Sache auszugleichen.

  2. Das zweite ist, dass ich das Front-End wirklich schon lange nicht mehr geschrieben habe, React Hooks nicht viel Praxis ist und Vue 3 nur den Composition API RFC und den vorherigen chinesischen Vue Function-based API RFC überflogen hat (also ich’ Ich bin mit den Details nicht so vertraut.) Sie können gerne Korrekturen und Ergänzungen vornehmen (und um Ergänzungen bitten).

Einführung

„Frameworks/Bibliotheken sind Abstraktionen von Programmiersprachen“ bedeutet nicht, dass Frameworks nicht ohne die Redewendungen und Programmierparadigmen der Sprache entworfen werden können, in der sie implementiert sind.

Dank mehrerer sehr Lisp-Funktionen von JavaScript: erstklassige Bürgerfunktionen, dynamische Typisierung und etwas Makrounterstützung (wie Babel) kann die Entwicklung von Front-End-Frameworks in den letzten Jahren viele Designideen für Programmiersprachen hervorbringen: Frameworks sind zu Syntax geworden Die aus DSLs und APIs bestehende Semantik wird aus JavaScript verworfen und durch APIs angehängt. Die Kombination aus der Laufzeit, die dem Betrieb des Systems zugrunde liegt, und dem mentalen Modell, das es ausdrückt.

Vue 3, „Reaktives (Abschluss-basiertes) OOP“

Beginnen wir mit Vue (Vue bezieht sich in diesem Artikel hauptsächlich auf Vue, das die Composition-API verwendet)

const Counter = {
  setup(initialProps) {
    const count = reactive({count: 0}) // or `ref(0)`
    const inc = () => { count.value++ }
    return {count, inc}
  }
  template: "..."
}

Die von Vue für Komponenten gewählte Semantik ist „Objekt“: Das Setup der Kompositions-API wird nur einmal aufgerufen und gibt ein Objekt zurück, das in die Counter-Komponente zusammengeführt werden soll. Dieses Objekt und seine Mitglieder sind alle dauerhafte Referenzen, einschließlich der im Inc-Abschluss gespeicherten Statusanzahl. dauerhaft. Beim Rendern handelt es sich um die Materialisierung des Vorlagenfelds auf der Komponente.

Die zusätzliche Kernsemantik von Vue ist „reaktiv“ (basierend auf veränderlichen Daten): Der Zustandszähler ist ein reaktives Objekt, die Art und Weise, wie Inc den Zustand ändert, besteht darin, den Zähler direkt zu ändern, und das Ergebnis der Zustandsänderung ist die Ausführung aller Beobachter (Watcher-)Logik, einschließlich erneutem Rendern und Durchführen von Nebenwirkungen (watchEffect), wird basierend auf dieser Semantik in den Datenfluss integriert.

Einige Studenten (wie der Fragesteller) sagten, dass Sie immer noch denken, dass dies die Semantik des Objekts ist, wenn Sie es ändern, um eine Renderfunktion zurückzugeben und Abschlüsse direkt zum Speichern von Komponentenvariablen verwenden?

return (props) => <a onClick={inc}>{count.value}</a>

Ja. Die Vue-Implementierung muss die Funktion und ihre Abschlussreferenz unverändert lassen (referenzielle Identität), um die Semantik des beibehaltenen Zustands zu erfüllen. Dies ist das klassische JavaScript-Muster, bei dem Abschlüsse zur Emulation der privaten Eigenschaften von Objekten verwendet werden. („Die Schließung ist das Ziel der Armen, und das Ziel ist die Schließung der Armen“)

Um Ihnen das Verständnis zu erleichtern: Was wäre, wenn wir eine hypothetische Vue-Sprache hätten?……

// hypothetical Vue lang
component Counter (props) {    // constructor
  @reactive count = 0          // field
  inc() { count.value ++ }     // method
  render() { return <a onClick={inc}>{count.value}</a> }
}

Hat klassenbasiertes OOP nicht eine Besonderheit? Außer, dass die Objekte von Vue in einem Singleton (oder Abschluss) und nicht in einer Klasse (oder einem Prototyp) implementiert sind und die Mitglieder reaktiv sind! Java).

Der Kern der Vue-Laufzeit ist die Abhängigkeitsverfolgung. Erstens macht sie die reaktive Semantik für den Benutzer relativ implizit und die Abhängigkeiten werden automatisch erfasst, was die mentale Belastung des Benutzers erheblich reduziert. Zweitens verfügt es über eine sehr feine Tracking-Granularität, und da Vue einen relativ hohen Grad an Statik verwendet, ermöglichen Vorlagen ein automatisches und sehr genaues Neu-Rendering.

Zusammenfassend lässt sich sagen, dass Vues mentales Modell in Bezug auf Komponenten immer noch „Objekte sind, die über Daten und Verhalten verfügen und auf sich selbst reagieren“. Solange Sie in dieser Denkrichtung denken, wird es einfacher zu verstehen, „warum Vues Zustand variable Datenstrukturen verwenden kann“. „Warum Vue einen Verweis benötigt, um Werttypen zu umschließen“ und was RFC beim Vergleich von React Hooks erwähnt hat, „Warum Vue näher am JS ist, an das jeder gewöhnt ist“ (das ist subjektiver), „Warum der GC-Druck von Vue geringer sein wird“, „Warum muss Vue Abhängigkeiten nicht manuell deklarieren“ und andere Vorteile ergeben sich daraus.

React: „Rein (halbmonadisches/algebraisches) FP“

Schauen wir uns React an (React bezieht sich in diesem Artikel hauptsächlich auf React unter der Hooks-API)

function Counter(props) {
  const [count, setCount] = React.useState(0);
  const inc = () => setCount(count + 1)
  return <a onClick={inc}>{count}</a>
}

Die Semantik, die React für Komponenten wählt, ist „Funktion“, und jedes Rendering ist ein echter Aufruf der Counter-Funktion. Jedes Mal, wenn useState ausgeführt wird und der aktuelle Status von React in count übernommen wird, wird jedes Mal eine neue Inc-Funktion erstellt (sodass der neue Zählwert in seinem Abschluss erfasst wird).

Die zusätzliche Kernsemantik von React ist ein „Bewertungskontext“ mit kontrollierten Nebenwirkungen. Laienhaft ausgedrückt handelt es sich um die laufende Umgebung von React: Die Statusanzahl muss jedes Mal aus dem React-Kontext entnommen werden, und die Art und Weise, wie inc den Status ändert, besteht darin, setCount zu verwenden. Aktualisieren Sie den Inhalt im Kontext. Aufgrund der Zustandsänderung wird diese Funktion erneut aufgerufen. Beim Aufruf ruft die Funktion den neuen Status aus dem neuen Kontext ab, rendert neu und plant kontextgesteuerte Nebenwirkungen (useEffect).

Um Ihnen das Verständnis zu erleichtern: Was wäre, wenn wir eine hypothetische React-Sprache hätten?……

// hypothetical React lang Ⅰ
component Counter = (props) =>      // function
  @context.state(1) {                  // context provides `get_or` and `put`
    count <- get_or(0)              // get from context (or use default value)
    let inc = () => put(count + 1)  // callback can update the context
    return <a onClick={inc}>{count}</a>
  }

Gibt es nicht einen Hinweis auf Monad-basiertes Haskell? Es ist nur so, dass React die API völlig autark macht, ohne dass Sie sich mit der Komplexität auseinandersetzen müssen. Wenn Sie mit dem Konzept von Monad als reinem FP nicht vertraut sind, können wir es ohne strenge Vorgaben als „Kontext“ des Textes verwenden. Der Grund, warum die M-Bombe geworfen wird, ist, dass sie oft als Maßstab für den Umgang mit Nebenwirkungen bei reinem FP verwendet wird und uns dabei hilft, zu zeigen, wie wir React auf reines FP reduzieren können.

Einige Studenten werden sich fragen, wie sich das von der „reinen Funktion“, die ich kenne, unterscheidet. Ist dies auch „reine funktionale Programmierung“?

component Counter = (props) =>
  context.state(1).get_or(0).then([count, put] => {  // `then` is monadic bind.
    let inc = () => put(count + 1)
    return <a onClock={inc}>{count}</a>
  }).unwrap() // assuming it's safe.

Haben Sie jemals an ein Versprechen mit einem asynchronen Kontext gedacht, auch Monad genannt?

Vereinfacht ausgedrückt kann man es sich als den unkompliziertesten Zustandsübergabestil vorstellen (tatsächlich hat das React-Team 2018 eine ähnliche API in Betracht gezogen, die eine von Sebs theoretischen Grundlagen darstellt):

component Counter = (props, stateMap) => {
  let count = stateMap.get(1, or=0);
  let inc = () => stateMap.set(1, count + 1); // functional update
  return <a onClick={inc}>{count}</a>
}

Das mentale Modell, dem React nähersteht und das von der Implementierung bis zum API-Design verfolgt wird, ist jedoch ein relativ neues reines FP-Konzept – der algebraische Effekt. Obwohl der Name ziemlich verwirrend klingt, beschreibt er tatsächlich Nebenwirkungen. Es ist weniger ausgefallen (weniger Keramik) und leichter zu verstehen als Monad. Dan hat einen sehr leicht verständlichen Blog-Beitrag für JS-Benutzer und eine chinesische Übersetzung. Wir können es uns zunächst als einen „versuchsweisen Fang, der wieder aufgenommen werden kann“ vorstellen.

Um Ihnen das Verständnis zu erleichtern: Was wäre, wenn wir eine andere hypothetische React-Sprache hätten?……

// hypothetical React lang Ⅱ
component Counter = (props) => {
  let count = perform getState(1),or=0); // 1. `perform` "throw" effects to the context
                                                // 4. resume with the continuation to here
  let inc = () => perform putState(1, s=count + 1);
  return <a onClick={inc}>{count}</a>
}

// call site
try <Counter /> handle  // 2.try-handle pattern match effects
                        // 3. get state from the context and then resume
| getState(key, or) => resume with context.state(key) ?? or
| putState(key, s) => context.state(key)=s; resume with void

Wir „werfen“ die Komponente aus, um den Status im „Ausführungskontext“ zu ändern, und „kehren“ dann wieder zurück …

Obwohl React große Anstrengungen unternommen hat, um die Eintrittsbarriere für APIs zu senken, scheint sein zunehmend rein funktionales Denken für immer mehr Programmierer sehr „abweichend“ zu sein.

Warum brauchen wir also „reine funktionale Programmierung“? Neben der deklarativen Formel, dem klaren Datenfluss, der lokalen Argumentation und der einfachen Kombinierbarkeit bietet die akademische theoretische Unterstützung dahinter eine sehr hohe theoretische Obergrenze und Vorteile in Bezug auf statische Analyse und Optimierung zur Kompilierungszeit, hohe Parallelität und hohe Parallelität Freundlichkeit zur Laufzeit (in den letzten Jahren wurde die theoretische Forschung zu Programmierprinzipien vollständig von der funktionalen Programmierung dominiert)

Der Kern der Laufzeit von React ist kooperatives Multitasking, das React um Low-Level-Funktionen wie Parallelität und Zeitplan erweitert. Viele Studenten haben nur von hoher Parallelität im Backend gehört, aber tatsächlich ist die „ultimative Benutzeroberfläche“ wie ein Multitasking-Betriebssystem ein Szenario mit hoher Parallelität und basiert auf Prozessplanung wie Zeitscheiben und Neupriorisierung. React möchte diese Technologien UI-Entwicklern zugänglich machen (z. B. Suspense, z. B. Selective Hydration, z. B. Fabrics in der neuen Architektur von RN). Der erste Schritt besteht darin, die Laufzeit mit der Fiber-Architektur neu zu schreiben, die nicht auf dem nativen Aufrufstapel basiert. und der zweite Schritt besteht darin, Hooks zu verwenden, um das Problem zu lösen, dass die Klassen-API in ihrer Reinheit nicht bindend genug ist. Unreine Komponenten sind im React-Parallelitätsmodus anfällig für Datenwettlaufprobleme.

Zusammenfassend lässt sich sagen, dass das mentale Modell von React in Bezug auf Komponenten „reine Funktionen sind, deren Nebenwirkungen durch den Kontext verwaltet werden“. Solange Sie in dieser Denkrichtung denken, wird es einfacher zu verstehen, „warum React dazu neigt, unveränderliche Datenstrukturen zu verwenden“ und „warum useEffect standardmäßig auf Cleanup ausgeführt wird, um die Idempotenz aufrechtzuerhalten“, „warum React eine übergreifende Datenstruktur benötigt“ Rendering-Ref-Zellmechanismus wie useRef, um veränderbare Referenzen zu erstellen“, „Warum umfasst die Leistungsoptimierung von React FP-artige Memoisierungen wie useMemo und Memo“, „Warum benötigt React useMemo useCallback, um die referenzielle Identität aufrechtzuerhalten“, „Warum muss React verwenden eine Abhängigkeitsliste für die Cache-Invalidierung“ und andere Fragen.

Ergänzung

Ende 2016 hatte ich das Gefühl, dass der ultimative Unterschied zwischen React und Vue „veränderlich“ oder „unveränderlich“ war.

Sebs Brain Dump, der nach der Veröffentlichung von Hooks auf einige Skepsis stieß, schrieb:

Das ist interessant, weil sich tatsächlich zwei Ansätze entwickeln. Es gibt einen Ansatz aus Veränderlichkeit + Änderungsverfolgung und einen Ansatz aus Unveränderlichkeit + referenziellem Gleichheitstest. Es ist schwierig, sie zu kombinieren, wenn Sie neue Funktionen darauf aufbauen. Aus diesem Grund hat React in letzter Zeit die Unveränderlichkeit etwas stärker vorangetrieben, um darauf aufbauen zu können. Bei beiden gibt es verschiedene Kompromisse, aber andere betreiben gute Forschung in anderen Bereichen, daher haben wir uns entschieden, uns auf diese Richtung zu konzentrieren und zu sehen, wohin sie uns führt.

Nicht alle davon wurden übersetzt, aber sie sind die beiden Hauptunterschiede in der gesamten „Big Frontend“-Community:

  • Variable + Änderungsverfolgung. Einschließlich Vue, Angular,
  • Unveränderliche + referenzielle Gleichheit. Beinhaltet React, Elm, (Flutter?)

Diese Divergenz ist eigentlich ein Kontrapunkt zu meiner vorherigen Betonung der „für Komponenten ausgewählten Semantik“:

  • Ersteres ist eine Erweiterung der traditionellen Idee der imperativen Programmierung (einschließlich OOP) mit der Hinzufügung von Reaktivität.
  • Letzteres ist die Erweiterung der traditionellen funktionalen Programmierung im Bereich der UI-Entwicklung (Functional Reactive Programming?), allerdings ist React auf eine Art und Weise implementiert, die eher „jenseits der JS-Sprache“ liegt.

Diese beiden Wege sind von Grund auf sehr unterschiedlich, weshalb React und Vue in aller Augen auseinanderdriften.

Allerdings habe ich in letzter Zeit mehr über Svelte, SwiftUI und Jetpack Compose gelesen und habe langsam das Gefühl, dass unterschiedliche Ansätze zum gleichen Ziel führen. Requisiten werden immer vorübergehend eingegeben, unabhängig davon, ob sie nach den Ansichts- oder Funktionsparametern zerstört werden, und Zustände werden immer vorübergehend eingegeben, unabhängig davon, ob sie der Komponenteninstanz folgen oder festgelegt werden. Der Devisenwechsel muss beständig sein. Was die Beurteilung von Aktualisierungen angeht, sind veränderliche Zustandsszenarien wie array.push immer schwer zu verfolgen, daher können wir nur unsere Talente zeigen: React möchte die Referenzgleichheit automatisch übergeben, Vue 3 möchte den Proxy automatisch übergeben, aber tatsächlich als Funktioniert Vue2/Svelte/SwiftUI/Compose nicht auch dann gut, wenn der Benutzer manuell Eingabeaufforderungen bereitstellen kann, solange der Änderungssatz erstellt werden kann? Solange der Änderungssatz berechnet und an die Ansichtsebene übergeben werden kann, wird die Ansichtsebene lediglich aktualisiert (erneut rendern/neu erstellen/neu zusammensetzen).

Beilage 2

Wenn Sie stark auf Flux (Vuex/Redux, was auch immer) angewiesen sind, ist Vue/React möglicherweise eher eine dumme/passive Ebene, die nur zum Rendern dient, und die oben erwähnten Vue/React-Unterschiede werden, wie bei den meisten anderen, nicht spürbar sein Die staatliche Verwaltung wurde auf die äußere Schicht geworfen.

Dieser Unterschied wird jedoch erst dann deutlicher, wenn man Szenarien betrachtet, in denen Komponentenkohäsion erforderlich ist (d. h. die Komponente selbst hat ihren eigenen privaten Status und muss selbstkontinuierlich sein) und die React Hooks/Vue Composition APIs beginnen, mehr Nebenwirkungen zu übernehmen (z. B. IO zusätzlich zu State).

Geschrieben von

Lifig Autor

Ich bin ein Daten-/Datenschutzingenieur mit Sitz in Sydney.