Im Jahr 2013 habe ich mir vorgenommen, für die Entwicklung von Webanwendungen zu erstellen. Das vielleicht beste Ergebnis dieses Prozesses war , ein clientseitiges, reines JS-Frontend-Framework, das in 2.000 Zeilen Code geschrieben wurde. ein minimalistisches Set an Tools gotoB Die Motivation zum Schreiben dieses Artikels kam, nachdem ich mich intensiv mit der Lektüre interessanter Artikel von Autoren sehr erfolgreicher Frontend-Frameworks befasst hatte: Zuerst war es Rich Harris, der über schrieb, die auf Signalen basieren. die Einführung von Runen in Svelte Das führte mich zu zwei Artikeln von Ryan Carniato, von denen einer die Entwicklung von erklärt und ein anderer . Signalen Signale beschreibt, insbesondere im Kontext von SolidJS Ryans Artikel führte mich zu zwei Artikeln von Michael Westrate, in denen er und beschreibt. die Prinzipien hinter MobX ihre Implementierung Was mich an diesen Artikeln so begeistert, ist die Tatsache, dass sie über die Entwicklung der sprechen, die hinter dem stehen, was sie bauen. Die Implementierung ist nur ein Weg, sie Wirklichkeit werden zu lassen, und die einzigen besprochenen Funktionen sind diejenigen, die so wesentlich sind, dass sie selbst Ideen darstellen. Ideen Der bei weitem interessanteste Aspekt von gotoB sind die Ideen, die sich aus den Herausforderungen bei der Entwicklung ergeben haben. Darauf möchte ich hier eingehen. Da ich das Framework von Grund auf neu erstellt habe und sowohl Minimalismus als auch interne Konsistenz erreichen wollte, habe ich vier Probleme auf eine Weise gelöst, die sich meiner Meinung nach von der Art und Weise unterscheidet, wie die meisten Frameworks dieselben Probleme lösen. Diese vier Ideen möchte ich jetzt mit Ihnen teilen. Ich tue dies nicht, um Sie davon zu überzeugen, meine Tools zu verwenden (obwohl Sie dies gerne tun können!), sondern vielmehr in der Hoffnung, dass Sie an den Ideen selbst interessiert sein könnten. Idee 1: Objektliterale zur Lösung von Templating-Problemen Jede Webanwendung muss je nach Status der Anwendung spontan Markup (HTML) erstellen. Dies lässt sich am besten anhand eines Beispiels erklären: In einer extrem einfachen To-Do-Listenanwendung könnte der Status eine Liste von Aufgaben sein: . Da Sie eine Anwendung schreiben (und keine statische Seite), muss die Liste der Aufgaben änderbar sein. ['Item 1', 'Item 2'] Da sich der Status ändert, muss sich das HTML, das die Benutzeroberfläche Ihrer Anwendung bildet, mit dem Status ändern. Um beispielsweise Ihre Aufgaben anzuzeigen, können Sie das folgende HTML verwenden: <ul> <li>Item 1</li> <li>Item 2</li> </ul> Wenn sich der Status ändert und ein drittes Element hinzugefügt wird, sieht Ihr Status nun folgendermaßen aus: ; Ihr HTML sollte dann folgendermaßen aussehen: ['Item 1', 'Item 2', 'Item 3'] <ul> <li>Item 1</li> <li>Item 2</li> <li>Item 3</li> </ul> Das Problem der HTML-Generierung basierend auf dem Status der Anwendung wird normalerweise mit einer gelöst, die Konstrukte der Programmiersprache (Variablen, Bedingungen und Schleifen) in Pseudo-HTML einfügt, das in eigentliches HTML erweitert wird. Template-Sprache Dies kann beispielsweise auf zwei Arten in verschiedenen Template-Tools erfolgen: // Assume that `todos` is defined and equal to ['Item 1', 'Item 2', 'Item 3'] // Moustache <ul> {{#todos}} <li>{{.}}</li> {{/todos}} </ul> // JSX <ul> {todos.map((item, index) => ( <li key={index}>{item}</li> ))} </ul> Ich war nie ein Fan dieser Syntaxen, die Logik in HTML brachten. Da mir klar wurde, dass die Erstellung von Templates Programmierung erforderte und ich eine separate Syntax dafür vermeiden wollte, beschloss ich stattdessen, HTML in js zu bringen, indem ich verwendete. So konnte ich mein HTML einfach als Objektliterale modellieren: Objektliterale ['ul', [ ['li', 'Item 1'], ['li', 'Item 2'], ['li', 'Item 3'], ]] Wenn ich dann die Liste durch Iteration generieren möchte, könnte ich einfach schreiben: ['ul', items.map ((item) => ['li', item])] Und dann verwenden Sie eine Funktion, die dieses Objekt wörtlich in HTML umwandelt. Auf diese Weise kann die gesamte Vorlagenerstellung in JS erfolgen, ohne Vorlagensprache oder Transpilierung. Ich verwende den Namen , um diese Arrays zu beschreiben, die HTML darstellen. „Liths“ Meines Wissens gibt es kein anderes JS-Framework, das auf diese Weise an die Vorlagenerstellung herangeht. Ich habe ein wenig nachgeforscht und gefunden, das fast dieselbe Struktur verwendet, um HTML in JSON-Objekten darzustellen (die fast dasselbe sind wie JS-Objektliterale), aber kein Framework gefunden, das darauf aufbaut. JSONML und kommen dem von mir verwendeten Ansatz recht nahe, verwenden aber dennoch Funktionsaufrufe für jedes Element. Mithril Hyperapp // Mithril m("ul", [ m("li", "Item 1"), m("li", "Item 2") ]) // hyperapp h("ul", [ h("li", "Item 1"), h("li", "Item 2") ]) Der Ansatz, Objektliterale zu verwenden, hat bei HTML gut funktioniert, daher habe ich ihn auf CSS erweitert und generiere jetzt auch mein gesamtes CSS über Objektliterale. Wenn Sie sich aus irgendeinem Grund in einer Umgebung befinden, in der Sie JSX nicht transpilieren oder eine Template-Sprache verwenden können, und Sie keine Zeichenfolgen verketten möchten, können Sie stattdessen diesen Ansatz verwenden. Ich bin mir nicht sicher, ob der Mithril/Hyperapp-Ansatz besser ist als meiner. Ich stelle fest, dass ich beim Schreiben langer Objektliterale, die Liths darstellen, manchmal irgendwo ein Komma vergesse und es manchmal schwierig sein kann, das zu finden. Ansonsten gibt es wirklich nichts zu beanstanden. Und ich finde es toll, dass die Darstellung für HTML sowohl 1) Daten als auch 2) JS ist. Diese Darstellung kann tatsächlich als virtueller DOM fungieren, wie wir sehen werden, wenn wir zu Idee Nr. 4 kommen. Bonusdetail: Wenn Sie HTML aus Objektliteralen generieren möchten, müssen Sie nur die folgenden beiden Probleme lösen: Entityify-Zeichenfolgen (d. h.: Escapen von Sonderzeichen). Wissen Sie, welche Tags geschlossen werden sollen und welche nicht. Idee 2: ein globaler, über Pfade adressierbarer Speicher zur Speicherung aller Anwendungszustände Komponenten haben mir noch nie gefallen. Wenn man eine Anwendung um Komponenten herum strukturieren will, muss man die Daten, die zur Komponente gehören, in der Komponente selbst platzieren. Das macht es schwierig oder sogar unmöglich, diese Daten mit anderen Teilen der Anwendung zu teilen. Bei Projekt, an dem ich gearbeitet habe, musste ich feststellen, dass ich immer einige Teile des Anwendungsstatus zwischen Komponenten teilen musste, die ziemlich weit voneinander entfernt waren. Ein typisches Beispiel ist der Benutzername: Sie benötigen ihn möglicherweise im Kontobereich und auch in der Kopfzeile. Wo gehört also der Benutzername hin? jedem Daher habe ich mich schon früh dazu entschlossen, ein einfaches Datenobjekt ( ) zu erstellen und dort alle meine Zustände unterzubringen. Ich habe es den genannt. Der Store enthält die Zustände aller Teile der App und kann daher von jeder Komponente verwendet werden. {} Store Dieser Ansatz galt in den Jahren 2013 bis 2015 als etwas ketzerisch, hat jedoch seitdem an Bedeutung gewonnen und sich sogar durchgesetzt. Was ich noch ziemlich neuartig finde, ist, dass ich verwende, um auf alle Werte im Store zuzugreifen. Wenn der Store beispielsweise: Pfade { user: { firstName: 'foo' lastName: 'bar' } } Ich kann einen Pfad verwenden, um beispielsweise auf den zuzugreifen, indem ich schreibe. Wie Sie sehen, ist der zu . ist eine Funktion, die auf den Store zugreift und einen bestimmten Teil davon zurückgibt, der durch den Pfad angegeben wird, den Sie an die Funktion übergeben. lastName B.get ('user', 'lastName') ['user', 'lastName'] Pfad 'bar' B.get Im Gegensatz zum oben Gesagten besteht der Standardweg zum Zugriff auf reaktive Eigenschaften darin, sie über eine JS-Variable zu referenzieren. Beispiel: // Svelte let { firstName, lastName } = $props(); firstName = 'foo'; lastName = 'bar'; // Knockout const firstName = ko.observable('foo'); const lastName = ko.observable('bar'); // mobx class UserStore { firstName = 'foo'; lastName = 'bar'; constructor() { makeAutoObservable(this); } } const userStore = new UserStore(); // SolidJS const [firstName, setFirstName] = createSignal('foo'); const [lastName, setLastName] = createSignal('bar'); Dies erfordert jedoch, dass Sie überall dort, wo Sie diesen Wert benötigen, einen Verweis auf und (oder ) beibehalten. Der von mir verwendete Ansatz erfordert lediglich, dass Sie Zugriff auf den Store haben (der global und überall verfügbar ist) und ermöglicht Ihnen einen feinkörnigen Zugriff darauf, ohne JS-Variablen dafür definieren zu müssen. firstName lastName userStore Immutable.js und die Firebase Realtime Database machen etwas, das dem, was ich gemacht habe, viel ähnlicher ist, obwohl sie an separaten Objekten arbeiten. Aber Sie könnten sie möglicherweise verwenden, um alles an einem einzigen Ort zu speichern, der granular adressierbar wäre. // Immutable.js let store = Map({ user: Map({ firstName: 'foo', lastName: 'bar' }) }); const firstName = store.getIn(['user', 'firstName']); // 'foo' // Firebase const db = firebase.database(); db.ref('user').set({ firstName: 'foo', lastName: 'bar' }); db.ref('user/firstName').once('value').then(snapshot => { const firstName = snapshot.val(); // 'foo' }); Meine Daten in einem global zugänglichen Speicher zu haben, auf den über Pfade granular zugegriffen werden kann, ist ein Muster, das ich als äußerst nützlich empfunden habe. Immer wenn ich oder etwas Ähnliches schreibe, fühlt es sich redundant an. Ich weiß, dass ich einfach ausführen könnte, wenn ich darauf zugreifen muss, ohne oder deklarieren und weitergeben zu müssen. const [count, setCount] = ... B.get ('count') count setCount Idee 3: Jede einzelne Veränderung wird durch Ereignisse ausgedrückt Wenn Idee Nr. 2 (ein globaler Speicher, auf den über Pfade zugegriffen werden kann) Daten von Komponenten befreit, dann ist Idee Nr. 3 die Art und Weise, wie ich Code von Komponenten befreit habe. Für mich ist das die interessanteste Idee in diesem Artikel. Hier ist sie! Unser Status besteht aus Daten, die per Definition veränderlich sind (für diejenigen, die Unveränderlichkeit verwenden, gilt das Argument weiterhin: Sie möchten immer noch, dass sich die Version des Status ändert, auch wenn Sie Snapshots älterer Versionen des Status aufbewahren). Wie ändern wir den Status? neueste Ich habe mich für Ereignisse entschieden. Ich hatte bereits Pfade zum Store, sodass ein Ereignis einfach die Kombination eines Verbs (wie , oder ) und eines Pfads sein konnte. Wenn ich also aktualisieren wollte, könnte ich etwa Folgendes schreiben: set add rem user.firstName B.call ('set', ['user', 'firstName'], 'Foo') Dies ist auf jeden Fall ausführlicher als Folgendes zu schreiben: user.firstName = 'Foo'; Aber es ermöglichte mir, Code zu schreiben, der auf eine Änderung von . Und das ist die entscheidende Idee: In einer Benutzeroberfläche gibt es verschiedene Teile, die von verschiedenen Teilen des Status abhängig sind. Sie könnten beispielsweise diese Abhängigkeiten haben: user.firstName reagiert Header: abhängig von und user currentView Kontobereich: abhängig vom user To-Do-Liste: hängt von ab items Die große Frage, die ich mir stellte, war: Wie aktualisiere ich den Header und den Kontobereich, wenn sich ändert, aber nicht, wenn sich ändern? Und wie verwalte ich diese Abhängigkeiten, ohne spezielle Aufrufe wie oder durchführen zu müssen? Diese Art von spezifischen Aufrufen stellt „jQuery-Programmierung“ in ihrer unhaltbarsten Form dar. user items updateHeader updateAccountSection Für mich schien es eine bessere Idee zu sein, so etwas zu tun: B.respond ('set', [['user'], ['currentView']], function (user, currentView) { // Update the header }); B.respond ('set', ['user'], function (user) { // Update the account section }); B.respond ('set', ['items'], function (items) { // Update the todo list }); Wenn also ein Ereignis für aufgerufen wird, benachrichtigt das Ereignissystem alle Ansichten, die an dieser Änderung interessiert sind (Header- und Kontobereich), während die anderen Ansichten (Aufgabenliste) unberührt bleiben. ist die Funktion, die ich verwende, um zu registrieren (die normalerweise als „Ereignislistener“ oder „Reaktionen“ bezeichnet werden). Beachten Sie, dass die Responder global sind und nicht an Komponenten gebunden sind. Sie hören jedoch nur auf Ereignisse auf bestimmten Pfaden. set user B.respond Responder set Wie wird nun überhaupt ein aufgerufen? So habe ich es gemacht: change B.respond ('set', '*', function () { // Assume that `path` is the path on which set was called B.call ('change', path); }); Ich vereinfache es ein wenig, aber so funktioniert es im Wesentlichen in gotoB. . Wenn Sie im obigen Beispiel aufrufen, werden zwei Codeteile ausgeführt: der, der den Header ändert, und der, der die Kontoansicht ändert. Beachten Sie, dass es dem Aufruf zum Aktualisieren „egal“ ist, wer dies abhört. Er macht einfach seine Arbeit und lässt den Antwortenden die Änderungen übernehmen. Was ein Ereignissystem leistungsfähiger macht als bloße Funktionsaufrufe, ist die Tatsache, dass ein Ereignisaufruf 0, 1 oder mehrere Codeteile ausführen kann, während ein Funktionsaufruf immer genau eine Funktion aufruft B.call ('set', ['user', 'firstName'], 'Foo'); firstName Ereignisse sind so leistungsfähig, dass sie meiner Erfahrung nach sowohl berechnete Werte als auch Reaktionen ersetzen können. Mit anderen Worten: Sie können verwendet werden, um jede Änderung auszudrücken, die in einer Anwendung erfolgen muss. Ein berechneter Wert kann mit einem Ereignis-Responder ausgedrückt werden. Wenn Sie beispielsweise einen berechnen und diesen nicht im Store verwenden möchten, können Sie Folgendes tun: fullName B.respond ('set', 'user', function () { var user = B.get ('user'); var fullName = user.firstName + ' ' + user.lastName; // Do something with `fullName` here. }); Ebenso können Reaktionen mit einem Responder ausgedrückt werden. Bedenken Sie Folgendes: B.respond ('set', 'user', function () { var user = B.get ('user'); var fullName = user.firstName + ' ' + user.lastName; document.getElementById ('header').innerHTML = '<h1>Hello, ' + fullName + '</h1>'; }); Wenn Sie für einen Moment die peinliche Verkettung von Zeichenfolgen zur Generierung von HTML außer Acht lassen, sehen Sie oben einen Responder, der einen „Nebeneffekt“ ausführt (in diesem Fall die Aktualisierung des DOM). (Randbemerkung: Was wäre eine gute Definition einer Nebenwirkung im Kontext einer Webanwendung? Für mich läuft es auf drei Dinge hinaus: 1) eine Aktualisierung des Status der Anwendung; 2) eine Änderung des DOM; 3) das Senden eines AJAX-Aufrufs). Ich habe festgestellt, dass es wirklich keinen Bedarf für einen separaten Lebenszyklus gibt, der das DOM aktualisiert. In gotoB gibt es einige Responder-Funktionen, die das DOM mithilfe einiger Hilfsfunktionen aktualisieren. Wenn sich also ändert, wird jeder davon abhängige Responder (oder genauer gesagt , da ich Responder, die einen Teil des DOM aktualisieren sollen, so nenne) ausgeführt, was einen Nebeneffekt erzeugt, der letztlich das DOM aktualisiert. user jede Ansichtsfunktion Ich habe das Ereignissystem vorhersehbar gemacht, indem ich die Responder-Funktionen in derselben Reihenfolge und nacheinander ausführen ließ. Asynchrone Responder können weiterhin synchron ausgeführt werden, und die Responder, die „nach“ ihnen kommen, warten auf sie. Anspruchsvollere Muster, bei denen Sie den Status aktualisieren müssen, ohne das DOM zu aktualisieren (normalerweise aus Leistungsgründen), können durch Hinzufügen wie hinzugefügt werden, die den Store ändern, aber keine Responder auslösen. Wenn Sie einem Neuzeichnen etwas am DOM tun müssen, können Sie einfach sicherstellen, dass dieser Responder eine niedrige Priorität hat und nach allen anderen Respondern ausgeführt wird: von Stummschaltverben mset nach B.respond ('set', 'date', {priority: -1000}, function () { var datePicker = document.getElementById ('datepicker'); // Do something with the date picker }); Der oben beschriebene Ansatz, ein Ereignissystem mit Verben und Pfaden und einer Reihe globaler Antwortprogramme zu verwenden, die von bestimmten Ereignisaufrufen abgeglichen (ausgeführt) werden, hat einen weiteren Vorteil: Jeder Ereignisaufruf kann in eine Liste eingetragen werden. Sie können diese Liste dann beim Debuggen Ihrer Anwendung analysieren und Änderungen am Status verfolgen. Im Kontext eines Frontends ermöglichen Ereignisse und Responder Folgendes: Um Teile des Stores mit sehr wenig Code zu aktualisieren (nur etwas ausführlicher als die bloße Variablenzuweisung). Teile des DOM sollen automatisch aktualisiert werden, wenn sich die Teile des Stores ändern, von denen dieser Teil des DOM abhängt. Damit kein Teil des DOM automatisch aktualisiert wird, wenn es nicht benötigt wird. Um berechnete Werte und Reaktionen haben zu können, die sich nicht auf die Aktualisierung des DOM beziehen, ausgedrückt als Responder. Auf Folgendes kann (meiner Erfahrung nach) verzichtet werden: Lebenszyklusmethoden oder Hooks. Observablen. Unveränderlichkeit. Auswendiglernen. Es handelt sich eigentlich nur um Ereignisaufrufe und Responder, wobei einige Responder nur mit Ansichten und andere mit anderen Vorgängen befasst sind. Alle internen Komponenten des Frameworks nutzen nur . den Benutzerbereich Wenn Sie neugierig sind, wie dies in gotoB funktioniert, können Sie sich diese ansehen. ausführliche Erklärung Idee 4: ein Text-Diff-Algorithmus zum Aktualisieren des DOM klingt jetzt ziemlich veraltet. Aber wenn Sie mit einer Zeitmaschine zurück ins Jahr 2013 reisen und das Problem der Neuzeichnung des DOM bei einer Statusänderung von Grund auf angehen, was würde dann vernünftiger klingen? Die bidirektionale Datenbindung Wenn sich das HTML ändert, aktualisieren Sie Ihren Status in JS. Wenn sich der Status in JS ändert, aktualisieren Sie das HTML. Aktualisieren Sie das HTML jedes Mal, wenn sich der Status in JS ändert. Wenn sich das HTML ändert, aktualisieren Sie den Status in JS und aktualisieren Sie das HTML dann erneut, damit es dem Status in JS entspricht. Tatsächlich klingt Option 2, also der unidirektionale Datenfluss vom Status zum DOM, komplizierter und ineffizienter. Lassen Sie uns das jetzt ganz konkret machen: Im Fall eines interaktiven oder , das fokussiert ist, müssen Sie Teile des DOM mit jedem Tastendruck des Benutzers neu erstellen! Wenn Sie unidirektionale Datenflüsse verwenden, löst jede Änderung der Eingabe eine Änderung des Status aus, die dann das neu zeichnet, damit es genau dem entspricht, was es sein sollte. <input> <textarea> <input> Dies legt die Messlatte für DOM-Updates sehr hoch: Sie sollten schnell sein und die Benutzerinteraktion mit interaktiven Elementen nicht behindern. Dies ist kein leicht zu lösendes Problem. Warum haben sich nun unidirektionale Daten vom Status zum DOM (JS zu HTML) durchgesetzt? Weil es einfacher zu begründen ist. Wenn sich der Status ändert, spielt es keine Rolle, woher diese Änderung kam (es könnte ein AJAX-Rückruf sein, der Daten vom Server bringt, es könnte eine Benutzerinteraktion sein, es könnte ein Timer sein). Der Status ändert sich (oder vielmehr ) immer auf die gleiche Weise. Und die Änderungen des Status fließen immer in das DOM ein. wird er mutiert Wie kann man also DOM-Updates effizient durchführen, ohne die Benutzerinteraktion zu behindern? Normalerweise läuft es darauf hinaus, so wenige DOM-Updates wie möglich durchzuführen, um die Aufgabe zu erledigen. Dies wird normalerweise als „Diffing“ bezeichnet, da Sie eine Liste der Unterschiede erstellen, die Sie benötigen, um eine alte Struktur (das vorhandene DOM) in eine neue zu konvertieren (das neue DOM nach der Aktualisierung des Status). Als ich 2016 anfing, an diesem Problem zu arbeiten, habe ich geschummelt, indem ich mir angesehen habe, was React tat. Dabei habe ich die entscheidende Erkenntnis gewonnen, dass es keinen verallgemeinerten, linearen Algorithmus zum Vergleichen zweier Bäume gab (der DOM ist ein Baum). Aber ich war stur und wollte trotzdem einen allgemeinen Algorithmus zum Vergleichen. Was mir an React (oder an fast jedem anderen Framework) besonders missfiel, war die Forderung, dass man Schlüssel für zusammenhängende Elemente verwenden muss: function MyList() { const items = ['Item 1', 'Item 2', 'Item 3']; return ( <ul> {items.map((item, index) => ( <li key={index}>{item}</li> ))} </ul> ); } Für mich war die Direktive überflüssig, da sie nichts mit dem DOM zu tun hatte, sondern nur ein Hinweis auf das Framework war. key Dann dachte ich darüber nach, einen textuellen Diff-Algorithmus an abgeflachten Versionen eines Baums auszuprobieren. Was wäre, wenn ich beide Bäume abflache (den alten DOM-Teil, den ich hatte, und den neuen DOM-Teil, durch den ich ihn ersetzen wollte) und einen darauf berechne (einen minimalen Satz von Änderungen), sodass ich in weniger Schritten vom alten zum neuen gelangen könnte? diff Also habe ich den genommen, den Sie jedes Mal verwenden, wenn Sie ausführen, und ihn auf meine abgeflachten Bäume angewendet. Lassen Sie uns das anhand eines Beispiels veranschaulichen: Myers-Algorithmus git diff var oldList = ['ul', [ ['li', 'Item 1'], ['li', 'Item 2'], ]]; var newList = ['ul', [ ['li', 'Item 1'], ['li', 'Item 2'], ['li', 'Item 3'], ]]; Wie Sie sehen, arbeite ich nicht mit dem DOM, sondern mit der Objektliteraldarstellung, die wir bei Idee 1 gesehen haben. Jetzt werden Sie feststellen, dass wir am Ende der Liste ein neues hinzufügen müssen. <li> Die abgeflachten Bäume sehen so aus: var oldFlattened = ['O ul', 'O li', 'L Item 1', 'C li', 'O li', 'L Item 2', 'C li', 'C ul']; var newFlattened = ['O ul', 'O li', 'L Item 1', 'C li', 'O li', 'L Item 2', 'C li', 'O li', 'L Item 3', 'C li', 'C ul']; Das steht für „open tag“, das für „literal“ (in diesem Fall ein Text) und das für „close tag“. Beachten Sie, dass jeder Baum jetzt eine Liste von Zeichenfolgen ist und es keine verschachtelten Arrays mehr gibt. Das meine ich mit Abflachen. O L C Wenn ich für jedes dieser Elemente einen Diff-Test ausführe (und dabei jedes Element im Array so behandle, als wäre es eine Einheit), erhalte ich: var diff = [ ['keep', 'O ul'] ['keep', 'O li'] ['keep', 'L Item 1'] ['keep', 'C li'] ['keep', 'O li'] ['keep', 'L Item 2'] ['keep', 'C li'] ['add', 'O li'] ['add', 'L Item 3'] ['add', 'C li'] ['keep', 'C ul'] ]; Wie Sie wahrscheinlich schon vermutet haben, behalten wir den größten Teil der Liste bei und fügen am Ende ein hinzu. Dies sind die , die Sie sehen. <li> add Wenn wir nun den Text des dritten von zu ändern und einen Diff-Test darauf ausführen würden, würden wir Folgendes erhalten: <li> Item 3 Item 4 var diff = [ ['keep', 'O ul'] ['keep', 'O li'] ['keep', 'L Item 1'] ['keep', 'C li'] ['keep', 'O li'] ['keep', 'L Item 2'] ['keep', 'C li'] ['keep', 'O li'] ['rem', 'L Item 3'] ['add', 'L Item 4'] ['keep', 'C li'] ['keep', 'C ul'] ]; Ich weiß nicht, wie mathematisch ineffizient dieser Ansatz ist, aber in der Praxis hat er recht gut funktioniert. Er funktioniert nur schlecht, wenn große Bäume verglichen werden, zwischen denen viele Unterschiede bestehen. Wenn das gelegentlich vorkommt, greife ich auf ein Timeout von 200 ms zurück, um das Vergleichen zu unterbrechen und den fehlerhaften Teil des DOM einfach vollständig zu ersetzen. Wenn ich kein Timeout verwenden würde, würde die gesamte Anwendung einige Zeit ins Stocken geraten, bis das Vergleichen abgeschlossen ist. Ein glücklicher Vorteil der Verwendung des Myers-Diffs ist, dass Löschungen gegenüber Einfügungen priorisiert werden: Das bedeutet, dass der Algorithmus zuerst ein Element entfernt, wenn es eine gleich effiziente Wahl zwischen dem Entfernen und dem Hinzufügen eines Elements gibt. In der Praxis ermöglicht mir dies, alle eliminierten DOM-Elemente zu erfassen und sie wiederzuverwenden, wenn ich sie später im Diff brauche. Im letzten Beispiel wird das letzte wiederverwendet, indem sein Inhalt von in geändert wird. Indem wir Elemente wiederverwenden (anstatt neue DOM-Elemente zu erstellen), verbessern wir die Leistung so weit, dass der Benutzer nicht merkt, dass das DOM ständig neu gezeichnet wird. <li> Item 3 Item 4 Wenn Sie sich fragen, wie komplex es ist, diesen Flattening- und Diffing-Mechanismus zu implementieren, der Änderungen am DOM vornimmt: Ich habe es mit 500 Zeilen ES5-JavaScript geschafft, und es läuft sogar in Internet Explorer 6. Aber zugegebenermaßen war es vielleicht der schwierigste Code, den ich je geschrieben habe. Sturheit hat ihren Preis. Abschluss Das sind die vier Ideen, die ich vorstellen wollte! Sie sind nicht ganz originell, aber ich hoffe, dass sie für manche sowohl neuartig als auch interessant sein werden. Danke fürs Lesen!