So implementieren Sie Reaktionsfähigkeit beim Lernen des Vue-Quellcodes

So implementieren Sie Reaktionsfähigkeit beim Lernen des Vue-Quellcodes

Vorwort

Unsere tägliche Arbeit als Frontend-Entwickler besteht darin, Daten auf der Seite darzustellen und Benutzerinteraktionen zu handhaben. In Vue wird die Seite neu gerendert, wenn sich die Daten ändern. Beispielsweise zeigen wir auf der Seite eine Zahl mit einer Klickschaltfläche daneben an. Jedes Mal, wenn wir auf die Schaltfläche klicken, erhöht sich die auf der Seite angezeigte Zahl um eins. Wie können wir das erreichen?
Gemäß der Logik von nativem JS sollten wir drei Dinge tun: Auf Klickereignisse achten, Daten in der Ereignisverarbeitungsfunktion ändern und dann das DOM manuell ändern, um es erneut zu rendern. Der größte Unterschied zwischen diesem und unserer Verwendung von Vue besteht darin, dass es einen weiteren Schritt gibt [das DOM manuell ändern, um es erneut zu rendern]. Dieser Schritt scheint einfach, aber wir müssen mehrere Probleme berücksichtigen:

  • Welches DOM muss geändert werden?
  • Muss ich das DOM jedes Mal ändern, wenn sich die Daten ändern?
  • Wie kann die Leistung der DOM-Änderung sichergestellt werden?

Daher ist es nicht einfach, ein responsives System zu implementieren. Lassen Sie uns die hervorragenden Ideen in Vue kennenlernen, indem wir den Vue-Quellcode kombinieren ~

1. Schlüsselelemente eines reaktionsfähigen Systems

1. So überwachen Sie Datenänderungen

Offensichtlich ist es sehr umständlich, Datenänderungen durch die Überwachung aller Benutzerinteraktionsereignisse zu erhalten, und einige Datenänderungen werden möglicherweise nicht von Benutzern ausgelöst. Wie überwacht Vue also Datenänderungen? ——Objekt.defineProperty

Warum kann die Methode Object.defineProperty Datenänderungen überwachen? Diese Methode kann direkt eine neue Eigenschaft für ein Objekt definieren oder eine vorhandene Eigenschaft eines Objekts ändern und das Objekt zurückgeben. Schauen wir uns zunächst die Syntax an:

Object.defineProperty(Objekt, Eigenschaft, Deskriptor)
// obj ist das übergebene Objekt, prop ist die zu definierende oder zu ändernde Eigenschaft, descriptor ist der Eigenschaftsdeskriptor

Der Kern ist hierbei der Deskriptor, der über viele optionale Schlüsselwerte verfügt. Was uns hier am meisten interessiert, sind get und set. Get ist eine Getter-Methode, die für eine Eigenschaft bereitgestellt wird. Wenn wir auf die Eigenschaft zugreifen, wird die Getter-Methode ausgelöst; set ist eine Setter-Methode, die für eine Eigenschaft bereitgestellt wird. Wenn wir die Eigenschaft ändern, wird die Setter-Methode ausgelöst.

Kurz gesagt, sobald ein Datenobjekt über Getter und Setter verfügt, können wir seine Änderungen einfach überwachen und es als responsives Objekt bezeichnen. Wie geht das konkret?

Funktion beobachten(Daten) {
  wenn (istObjekt(Daten)) {
    Objekt.Schlüssel(Daten).fürJeden(Schlüssel => {
      defineReactive(Daten, Schlüssel)
    })
  }
}

Funktion defineReactive(Objekt, Eigenschaft) {
  let val = obj[Eigenschaft]
  let dep = new Dep() // Wird zum Sammeln von Abhängigkeiten verwendetObject.defineProperty(obj, prop, {
    erhalten() {
      // Der Zugriff auf Objekteigenschaften zeigt an, dass die aktuellen Objekteigenschaften abhängig sind und die Abhängigkeiten gesammelt werden dep.depend()
      Rückgabewert
    }
    setze(neuerWert) {
      wenn (neuerWert === Wert) zurückgeben
      // Die Daten wurden geändert. Es ist Zeit, das zuständige Personal zu benachrichtigen, damit die entsprechenden Ansichten aktualisiert werden. val = newVal
      dep.benachrichtigen()     
    }
  }) 
  // Umfassende Überwachung if (isObject(val)) {
    beobachten(Wert)
  }
  Rückgabeobjekt
}

Hier benötigen wir eine Dep-Klasse (Abhängigkeit), um die Abhängigkeitssammlung durchzuführen🎭

PS: Object.defineProperty kann nur vorhandene Eigenschaften überwachen, ist aber bei neu hinzugefügten Eigenschaften machtlos. Es kann auch keine Änderungen in Arrays überwachen (dieses Problem wird in Vue2 durch Umschreiben der Methode im Array-Prototyp gelöst), daher wird es in Vue3 durch einen leistungsstärkeren Proxy ersetzt.

2. So sammeln Sie Abhängigkeiten - implementieren Sie die Dep-Klasse

Basierend auf der Konstruktorimplementierung:

Funktion Dep() {
  // Verwenden Sie das Deps-Array, um verschiedene Abhängigkeiten zu speichern. this.deps = []
}
// Dep.target wird verwendet, um die laufende Watcher-Instanz aufzuzeichnen, die ein global eindeutiger Watcher ist 
// Dies ist ein sehr cleveres Design, da JS ein Single-Thread ist und nur ein globaler Watcher gleichzeitig berechnet werden kann. Dep.target = null

// Definieren Sie die Depend-Methode für den Prototyp, und jede Instanz kann darauf zugreifen Dep.prototype.depend = function() {
  wenn (Dep.target) {
    dies.deps.push(Dep.target)
  }
}
// Definieren Sie die Benachrichtigungsmethode für den Prototyp, um den Beobachter über die Aktualisierung zu informieren. Dep.prototype.notify = function() {
  dies.deps.forEach(watcher => {
    watcher.update()
  })
}
// In Vue wird es verschachtelte Logik geben, z. B. Komponentenverschachtelung. Verwenden Sie daher den Stapel, um den verschachtelten Watcher aufzuzeichnen. 
// Stapel, zuerst rein, zuletzt raus const targetStack = [] 
Funktion pushTarget(_target) { 
  wenn (Dep.target) ZielStack.push(Dep.target) 
  Dep.Ziel = _Ziel 
} 
Funktion popTarget() { 
  Dep.target = ZielStack.pop() 
}

Hier verstehen wir hauptsächlich zwei Methoden des Prototyps: „depend“ und „notify“, eine zum Hinzufügen von Abhängigkeiten und die andere zum Benachrichtigen von Aktualisierungen. Wir sprechen über das Sammeln von „Abhängigkeiten“. Was genau wird also im Array this.deps gespeichert? Vue richtet das Konzept des Watchers zur Abhängigkeitsdarstellung ein, d. h., was in this.deps gesammelt wird, sind Watcher.

3. So aktualisieren Sie bei Datenänderungen - Implementierung der Watcher-Klasse

Es gibt drei Arten von Watchern in Vue, die für die Seitendarstellung und die beiden APIs „Computed“ und „Watch“ verwendet werden. Zur Unterscheidung werden Watcher mit unterschiedlicher Verwendung jeweils als „RenderWatcher“, „ComputedWatcher“ und „WatchWatcher“ bezeichnet.

Implementieren Sie es mit der Klasse:

Klasse Watcher {
  Konstruktor(expOrFn) {
    // Wenn der hier übergebene Parameter keine Funktion ist, muss er analysiert werden, parsePath wird weggelassen this.getter = typeof expOrFn === 'function' ? expOrFn : parsePath(expOrFn)
    dies.get()
  }
  // Es ist nicht erforderlich, beim Definieren von Funktionen in der Klasse eine Funktion zu schreiben.
  erhalten() {
    // An diesem Punkt der Ausführung ist dies die aktuelle Watcher-Instanz und auch Dep.target
    drückeZiel(dieses)
    dieser.Wert = dieser.Getter()
    popZiel()
  }
  aktualisieren() {
    dies.get()
  }
}

An diesem Punkt hat ein einfaches reaktionsfähiges System Gestalt angenommen. Zusammengefasst: Object.defineProperty ermöglicht es uns zu wissen, wer auf die Daten zugreift und wann sich die Daten ändern, Dep kann aufzeichnen, welche DOMs mit bestimmten Daten verknüpft sind, und Watcher kann das DOM benachrichtigen, damit es aktualisiert wird, wenn sich die Daten ändern.
Watcher und Dep sind eine Implementierung eines sehr klassischen Beobachter-Entwurfsmusters.

2. Virtueller DOM und Diff

1. Was ist ein virtueller DOM?

Virtuelles DOM verwendet Objekte in JS, um das reale DOM darzustellen. Wenn sich Daten ändern, ändern Sie diese zuerst im virtuellen DOM und dann im realen DOM. Gute Idee! 💡

Bezüglich der Vorteile von virtuellem DOM ist es besser, auf Youda zu hören:

Meiner Meinung nach lag der wahre Wert von Virtual DOM nie in der Leistung, sondern darin, dass es 1) die Tür zur funktionalen UI-Programmierung öffnet und 2) auf einem anderen Backend als DOM rendern kann.

Zum Beispiel:

<Vorlage>
  <div id="Anwendung" Klasse="Container">
    <h1>HALLO WELT! </h1>
  </div>
</Vorlage>
// Entsprechender vnode 
{ 
  Tag: 'div', 
  Requisiten: { ID: 'App', Klasse: 'Container' }, 
  untergeordnete Elemente: { tag: ‚h1‘, untergeordnete Elemente: ‚HALLO WELT!‘ ' } 
}

Wir können es wie folgt definieren:

Funktion VNode(Tag, Daten, Kinder, Text, Ulme) { 
  this.tag = Tag 
  this.data = Daten 
  this.childern = Kinder 
  dieser.text = Text 
  this.elm = elm // Verweis auf den realen Knoten}

2. Diff-Algorithmus - Vergleich zwischen neuen und alten Knoten

Wenn sich die Daten ändern, wird der Rendering Watcher-Callback ausgelöst und die Ansicht aktualisiert. Im Vue-Quellcode wird die Patch-Methode verwendet, um beim Aktualisieren der Ansicht die Ähnlichkeiten und Unterschiede zwischen neuen und alten Knoten zu vergleichen.

(1) Bestimmen Sie, ob der neue und der alte Knoten dieselben Knoten sind

Funktion sameVNode()
Funktion sameVnode(a, b) { 
  Rückgabewert a.Schlüssel === b.Schlüssel && 
  ( a.tag === b.tag && 
    a.istKommentar === b.istKommentar && 
    isDef(a.data) === isDef(b.data) && 
    gleicherEingabetyp(a, b) 
  ) 
 }

(2) Wenn die neuen und alten Knoten unterschiedlich sind

Alten Knoten ersetzen: neuen Knoten erstellen --> alten Knoten löschen

(3) Wenn der neue und der alte Knoten gleich sind

  • Es gibt keine untergeordneten Knoten, daher ist es einfach zu sagen
  • Einer hat untergeordnete Knoten, der andere nicht. Es ist einfach zu sagen: Entweder lösche den untergeordneten Knoten oder füge einen neuen untergeordneten Knoten hinzu.
  • Alle haben untergeordnete Knoten, was etwas kompliziert ist. Führen Sie updateChildren aus:
Funktion updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
  let oldStartIdx = 0
  let newStartIdx = 0
  let oldEndIdx = alteCh.Länge - 1
  let oldStartVnode = oldCh[0]
  let oldEndVnode = oldCh[oldEndIdx]
  let newEndIdx = newCh.length - 1
  lass newStartVnode = newCh[0]
  let newEndVnode = newCh[newEndIdx]
  let oldKeyToIdx, idxInOld, vnodeToMove, refElm
  // Das Obige sind die Kopf- und Endzeiger der neuen und alten Vnodes, die Kopf- und Endknoten der neuen und alten Vnodes while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    // Wenn die while-Bedingung nicht erfüllt ist, bedeutet dies, dass mindestens einer der alten und neuen Vnodes einmal durchlaufen wurde. Anschließend verlassen wir die Schleife, wenn (isUndef(oldStartVnode)) {
      oldStartVnode = oldCh[++oldStartIdx] // Vnode wurde nach links verschoben
    } sonst wenn (isUndef(oldEndVnode)) {
      oldEndVnode = alterCh[--oldEndIdx]
    } sonst wenn (gleicherVnode(alterStartVnode, neuerStartVnode)) {
      // Vergleichen Sie den alten Start und den neuen Start, um zu sehen, ob es sich um denselben Knoten handelt: patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
      alterStartVnode = alterCh[++alteStartIdx]
      neuerStartVnode = neuerCh[++neueStartIdx]
    } sonst wenn (gleicherVnode(alterVnodeEndknoten, neuerVnodeEndknoten)) {
      // Vergleichen Sie die alten und neuen Enden, um zu sehen, ob es sich um denselben Knoten handelt: patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
      oldEndVnode = alterCh[--oldEndIdx]
      neuerEndVnode = neuerCh[--newEndIdx]
    } sonst wenn (sameVnode(oldStartVnode, newEndVnode)) { // Vnode nach rechts verschoben
      // Vergleichen Sie den alten Anfang und das neue Ende, um zu sehen, ob es sich um denselben Knoten handelt: patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
      kann verschieben && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
      alterStartVnode = alterCh[++alteStartIdx]
      neuerEndVnode = neuerCh[--newEndIdx]
    } sonst wenn (sameVnode(oldEndVnode, newStartVnode)) { // Vnode nach links verschoben
      // Vergleichen Sie das alte Ende und den neuen Anfang, um zu sehen, ob es sich um denselben Knoten handelt: patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
      kann verschieben und nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
      oldEndVnode = alterCh[--oldEndIdx]
      neuerStartVnode = neuerCh[++neueStartIdx]
    } anders {
      // Der Unterschied zwischen dem Festlegen eines Schlüssels und dem Nichtfestlegen eines Schlüssels:
      // Ohne den Schlüssel zu setzen, vergleichen newCh und oldCh nur Kopf und Ende. Nach dem Setzen des Schlüssels wird zusätzlich zum Vergleich zwischen Kopf und Ende der passende Knoten aus dem vom Schlüssel generierten Objekt oldKeyToIdx gefunden. Daher kann das Setzen von Schlüsseln für Knoten DOM effizienter nutzen.
      wenn (isUndef(alterKeyToIdx)) alterKeyToIdx = createKeyToOldIdx(alterCh, alteStartIdx, alteEndIdx)
      idxInOld = isDef(neuerStartVnode.key)
        ? oldKeyToIdx[neuerStartVnode.key]
        : findIdxInOld(neuerStartVnode, alterCh, alteStartIdx, alteEndIdx)
      // Extrahiere die Knoten mit Schlüsseln aus der oldVnode-Sequenz und füge sie in die Karte ein, dann durchlaufe die neue vnode-Sequenz // Bestimmen Sie, ob der Schlüssel des vnode in der Karte ist. Wenn ja, suchen Sie den oldVnode, der dem Schlüssel entspricht. Wenn der oldVnode derselbe ist wie der durchlaufe vnode, verwenden Sie den Dom erneut und verschieben Sie die Dom-Knotenposition if (isUndef(idxInOld)) { // Neues Element
        createElm(neuerStartVnode, eingefügteVnodeQueue, übergeordneteElm, alterStartVnode.elm, false, neuerCh, neueStartIdx)
      } anders {
        vnodeToMove = alterCh[idxInOld]
        wenn (sameVnode(vnodeToMove, newStartVnode)) {
          patchVnode(ZuVerschiebender vnode, neuerStartVnode, eingefügteVnodeQueue)
          oldCh[idxInOld] = undefiniert
          kann verschieben und nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
        } anders {
          // gleicher Schlüssel, aber anderes Element. Als neues Element behandeln
          createElm(neuerStartVnode, eingefügteVnodeQueue, übergeordneteElm, alterStartVnode.elm, false, neuerCh, neueStartIdx)
        }
      }
      neuerStartVnode = neuerCh[++neueStartIdx]
    }
  }
  wenn (alteStartIdx > alteEndIdx) {
    refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
    addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, eingefügteVnodeQueue)
  } sonst wenn (neueStartIdx > neueEndIdx) {
    removeVnodes(übergeordneteElm, alterCh, alteStartIdx, alteEndIdx)
  }
}

Die Hauptlogik hier ist: Vergleichen Sie den Kopf und das Ende des neuen Knotens mit dem Kopf und dem Ende des alten Knotens, um zu sehen, ob es sich um dieselben Knoten handelt. Wenn dies der Fall ist, patchen Sie Vnode direkt; andernfalls verwenden Sie eine Map, um die Schlüssel der alten Knoten zu speichern, und durchlaufen Sie dann die Schlüssel der neuen Knoten, um zu sehen, ob sie in den alten Knoten vorhanden sind. Wenn sie gleich sind, verwenden Sie sie erneut; die Zeitkomplexität ist hier O(n) und die Raumkomplexität ist ebenfalls O(n), wobei der Raum zum Tauschen von Zeit verwendet wird~

Der Diff-Algorithmus wird hauptsächlich verwendet, um die Anzahl der Aktualisierungen zu reduzieren, den DOM mit dem geringsten Unterschied zu finden und nur den Unterschied zu aktualisieren.

3. nächsterTick

Der sogenannte nextTick bedeutet den nächsten Tick. Was also ist ein Tick?

Wir wissen, dass die Ausführung von JS einfädig erfolgt. Es verarbeitet asynchrone Logik basierend auf der Ereignisschleife, die hauptsächlich in die folgenden Schritte unterteilt ist:

  1. Alle synchronen Aufgaben werden auf dem Hauptthread ausgeführt und bilden einen Ausführungskontextstapel.
  2. Neben dem Hauptthread gibt es auch eine „Task-Warteschlange“. Solange die asynchrone Aufgabe ein laufendes Ergebnis hat, wird ein Ereignis in die „Aufgabenwarteschlange“ gestellt.
  3. Sobald alle synchronen Aufgaben im „Ausführungsstapel“ ausgeführt wurden, liest das System die „Aufgabenwarteschlange“, um zu sehen, welche Ereignisse sich darin befinden. Die entsprechenden asynchronen Aufgaben beenden dann den Wartezustand, gelangen in den Ausführungsstapel und beginnen mit der Ausführung.
  4. Der Hauptthread wiederholt ständig Schritt 3 von oben.

Der Ausführungsprozess des Hauptthreads ist ein Häkchen, und alle asynchronen Ergebnisse werden über die "Task-Warteschlange" geplant. In der Nachrichtenwarteschlange werden die Aufgaben einzeln gespeichert. Die Spezifikation legt fest, dass Aufgaben in zwei Kategorien unterteilt werden, nämlich Makroaufgaben und Mikroaufgaben, und dass alle Mikroaufgaben nach Abschluss jeder Makroaufgabe gelöscht werden müssen.

für (MakroTask von MakroTaskQueue) { 
  // 1. Aktuelle MACRO-TASK bearbeiten 
  handleMacroTask()
  // 2. Erledigen Sie alle MICRO-TASK 
  für (Mikrotask von Mikrotaskqueue) { 
    Mikrotask handhaben (Mikrotask)
  } 
}

Zu den gängigen Makroaufgaben in der Browserumgebung zählen „setTimeout“, „MessageChannel“, „postMessage“, „setImmediate“ und „setInterval“; zu den gängigen Mikroaufgaben zählen „MutationObsever“ und „Promise.then“.

Wir wissen, dass das erneute Rendern des DOM aufgrund von Datenänderungen ein asynchroner Prozess ist, der im nächsten Tick erfolgt. Wenn wir beispielsweise während des Entwicklungsprozesses Daten von der Serverschnittstelle abrufen, werden die Daten geändert. Wenn einige unserer Methoden auf DOM-Änderungen angewiesen sind, nachdem die Daten geändert wurden, müssen wir sie nach nextTick ausführen. Beispielsweise der folgende Pseudocode:

getData(res).then(() => { 
  dies.xxx = res.data 
  this.$nextTick(() => { // Hier können wir das geänderte DOM abrufen }) 
})

IV. Fazit

Dies ist das Ende dieses Artikels über die Implementierung von Reaktionsfähigkeit beim Lernen von Vue-Quellcode. Weitere relevante Inhalte zur Implementierung von Reaktionsfähigkeit in Vue finden Sie in früheren Artikeln auf 123WORDPRESS.COM oder in den folgenden verwandten Artikeln. Ich hoffe, dass jeder 123WORDPRESS.COM in Zukunft unterstützen wird!

Das könnte Sie auch interessieren:
  • Vue fügt Array- und Objektwerte hinzu und ändert sie reaktionsschnell
  • Eine kurze Diskussion zur Reaktionsfähigkeit von Vue (Array-Mutationsmethode)
  • Vue.js muss jeden Tag lernen, das Prinzip der internen Reaktionsfähigkeit zu erkunden
  • Eine kurze Diskussion zum Reaktionsfähigkeitsprinzip von Vue
  • Sprechen Sie über das Missverständnis der Vue Responsive Data Update
  • So implementieren Sie ein reaktionsfähiges System in Vue
  • Detaillierte Erklärung des Reaktionsprinzips von Vue
  • Detaillierte Erläuterung des Vue3.0-Datenreaktionsprinzips
  • Eine kurze Diskussion über das Prinzip der Vue-Datenreaktionsfähigkeit

<<:  Verwendung der MySQL SHOW STATUS-Anweisung

>>:  Perfekte Lösung für das Problem, unter Windows 10 nicht auf den Port des Docker-Containers zugreifen zu können

Artikel empfehlen

So erstellen Sie ein Apache-Image mit Dockerfile

Inhaltsverzeichnis 1. Docker-Image 2. Erstellen S...

Einführung in die Verwendung von this in HTML-Tags

Zum Beispiel: Code kopieren Der Code lautet wie fo...

JavaScript zur Implementierung eines einziehbaren sekundären Menüs

Der spezifische Code zur Implementierung des einz...

So verwenden Sie das Schreiben von Dateien zum Debuggen einer Linux-Anwendung

Unter Linux ist alles eine Datei, daher besteht d...

Miniprogramm zur Implementierung der Sieve-Lotterie

In diesem Artikelbeispiel wird der spezifische Co...

Zusammenfassung gängiger Toolbeispiele in MySQL (empfohlen)

Vorwort Dieser Artikel stellt hauptsächlich die r...