Advertisement
  1. Code
  2. HTML5

HTML5-Beherrschung: Baumdurchquerung

Scroll to top
Read Time: 13 min
This post is part of a series called HTML5 Mastery Class.
HTML5 Mastery: Fragments
HTML5 Mastery: Constraint Validation

() translation by (you can also view the original English article)

HTML5 Mastery series imageHTML5 Mastery series imageHTML5 Mastery series image

Eines der wichtigsten Konzepte im DOM ist das Durchqueren von Bäumen. Seit sich die Informatik als eigenes Studienfach etabliert hat, wurde jahrzehntelang an Datenstrukturen und Algorithmen geforscht. Eine der am häufigsten verwendeten Strukturen ist ein Baum. Bäume sind überall. Eine sehr rudimentäre, aber nützliche und oft verwendete Version ist ein Binärbaum. Ein Turnier kann als binärer Baum dargestellt werden. Der DOM-Baum ist jedoch nicht binär. Stattdessen ist es ein K-ary-Baum. Jeder Knoten kann null bis N Unterknoten haben, die childNodes genannt werden.

Ein DOM-Baum beherbergt eine Vielzahl möglicher Knotentypen. Es kann unter vielen Text, Element, Comment und andere spezielle wie ProcessingInstruction oder DocumentType geben. Die meisten von ihnen haben per Definition keine childNodes. Sie sind Endpunkte und tragen nur eine einzige Information. Ein Comment-Knoten trägt beispielsweise nur die angegebene Kommentarzeichenfolge. Ein Text-Knoten ist nur zum Speichern einer Inhaltszeichenfolge verfügbar.

Der Element-Knoten hostet andere Knoten. Wir können rekursiv von Element zu Element absteigen, um alle verfügbaren Knoten im System zu durchlaufen.

Ein anschauliches Beispiel

Ein Beispiel, das sich auch auf den vorherigen Artikel zum <template>-Element bezieht, ist das Ausfüllen einer DOM-Teilbaumstruktur. Ein Teilbaum ist ein Teil des Baums, der bei einem bestimmten Element beginnt. Wir nennen das angegebene Element die Unterbaumwurzel. Wenn wir das <html>-Element eines Baums als Unterbaumwurzel nehmen, wäre der Unterbaum fast identisch mit dem echten Baum, der beim document beginnt, also eine Ebene unter dem documentElement.

Das Ausfüllen der Unterbaumstruktur erfordert, dass wir alle Kinder der Unterbaumwurzel durchlaufen. Auf jedem Knoten müssen wir den genauen Knotentyp überprüfen und dann auf die entsprechende Weise fortfahren. Zum Beispiel muss jedes Element wieder als Teilbaumwurzel betrachtet werden. Textknoten hingegen müssen sorgfältiger evaluiert werden. Vielleicht wollen wir auch die Kommentarknoten auf spezielle Direktiven überprüfen. Darüber hinaus sollten auch die Attribute der Elemente berücksichtigt werden.

Für das Szenario verwenden wir eine Methode namens applyModel, um auf Vorlagen basierende Zeichenfolgen mit Werten aus einem Modell zu füllen. Das Verfahren sieht wie folgt aus und könnte natürlich noch weiter optimiert werden. Dennoch ist es für unsere Zwecke sicherlich ausreichend.

1
function applyModel(model, data) {
2
	var rx = new RegExp('\\{\\s*(.+?)\\s*\\}', 'g');
3
	var group = rx.exec(data);
4
	
5
	while (group) {
6
		var name = group[1];
7
		var value = '';
8
		eval('with (model) { value = ' + name + '; }');
9
		data = data.replace(group[0], value);
10
		group = rx.exec(data);
11
	}
12
13
	return data;
14
}

Sehen wir uns eine Implementierung für das beschriebene Szenario an, die die Methode applyModel bei verschiedenen Gelegenheiten verwendet. Dies erfordert eine Instanz eines template-Elements und ein Objekt namens model, um ein neues DocumentFragment zurückzugeben. Das neue Fragment verwendet die Daten aus dem Modell, um alle Werte von {X} in das Ergebnis der Auswertung des Ausdrucks X mit dem bereitgestellten Objekt zu ändern.

1
function iterateClassic (template, model) {
2
	var fragment = template.content.clone(true);
3
	var allNodes = findAllNodes(fragment);
4
5
	allNodes.forEach(changeNode);
6
	return fragment;
7
}

Der vorherige Code verwendet eine Funktion findAllNodes, die einen Knoten nimmt und alle seine Kinder in einem Array speichert. Die Funktion wird dann für jedes Kind rekursiv aufgerufen. Am Ende werden alle Ergebnisse zu einem einzigen Array des gesamten Teilbaums zusammengeführt, d. h. wir transformieren die Baumstruktur in ein 1-dimensionales Array.

Der folgende Codeausschnitt zeigt eine Beispielimplementierung für den beschriebenen Algorithmus.

1
function findAllNodes (childNodes) {
2
	var nodes = [];
3
4
	if (childNodes && childNodes.length > 0) {
5
		for (var i = 0, length = childNodes.length; i < length; i++) {
6
			nodes.push(childNodes[i]);
7
			nodes = nodes.concat(findAllNodes(childNodes[i].childNodes));
8
		}
9
	}
10
11
	return nodes;
12
}

Die Funktion zum Ändern jedes Knotens im Array wird unten gezeigt. Die Funktion führt je nach Art des Knotens einige Manipulationen durch. Wir kümmern uns nur um Attribute und Textknoten.

1
function changeNode (node) {
2
	switch (node.nodeType) {
3
		case Node.TEXT_NODE:
4
			node.text.data = applyModel(model, node.text.data);
5
			break;
6
		case Node.ELEMENT_NODE:
7
			Array.prototype.forEach.call(node.attributes, function (attribute) {
8
				attribute.value = applyModel(model, attribute.value);
9
			});
10
			break;
11
	}
12
}

Obwohl der Code leicht zu verstehen ist, ist er nicht sehr schön. Wir haben einige Performance-Probleme, zumal wir viele leider notwendige DOM-Operationen haben. Dies kann mit einem der DOM-Baum-Helfer effizienter durchgeführt werden. Beachten Sie, dass die Methode findAllNodes ein Array mit allen Knoten zurückgibt, nicht nur mit allen Element-Instanzen im Unterbaum. Wenn wir an letzterem interessiert sind, können wir einfach einen querySelectorAll('*')-Aufruf verwenden, der die Iteration für uns durchführt.

Iterieren über Knoten

Eine sofort verfügbare Lösung ist die Verwendung eines NodeIterator. Ein NodeIterator iteriert über Knoten. Es passt fast perfekt zu unseren Kriterien. Wir können einen neuen NodeIterator erstellen, indem wir die createNodeIterator-Methode des document-Objekts verwenden. Es gibt drei entscheidende Parameter:

  1. Der Wurzelknoten des zu iterierenden Unterbaums.
  2. Filter, über welche Knoten ausgewählt / iteriert werden soll.
  3. Ein Objekt mit einem acceptNode für die benutzerdefinierte Filterung.

Während das erste Argument nur ein einfacher DOM-Knoten ist, verwenden die anderen beiden spezielle Konstanten. Sollen beispielsweise alle Knoten angezeigt werden, müssen wir -1 als Filter übergeben. Alternativ können wir NodeFilter.SHOW_ALL verwenden. Wir könnten mehrere Filter auf verschiedene Weise kombinieren. Als Beispiel kann die Kombination der Anzeige aller Kommentare und aller Elemente mit ausgedrückt werden NodeFilter.SHOW_COMMENT | .  NodeFilter.SHOW_ELEMENT.

Das dritte Argument ist ein Objekt, das so primitiv aussehen kann wie der folgende Codeausschnitt. Auch wenn das Objekt, das die Funktion umschließt, redundant zu sein scheint, wurde es so angegeben. Einige Browser, z.B. Mozilla Firefox, geben uns die Möglichkeit, das Objekt auf eine einzige Funktion zu reduzieren.

1
var acceptAllNodes = {
2
	acceptNode: function (node) { 
3
		return NodeFilter.FILTER_ACCEPT;
4
	}
5
};

Hier akzeptieren wir jeden übergebenen Knoten. Zusätzlich haben wir die Möglichkeit, einen Knoten (und seine Kinder) mit der Option FILTER_REJECT abzulehnen. Wenn wir den Knoten nur überspringen möchten, aber dennoch an seinen untergeordneten Elementen interessiert sind, können wir die Konstante FILTER_SKIP verwenden.

Die Implementierung unseres vorherigen Beispiels mit dem NodeIterator ist ziemlich einfach. Wir erstellen einen neuen Iterator, indem wir die Konstruktormethode im document verwenden. Dann verwenden wir die nextNode-Methode, um über alle Knoten zu iterieren.

Schauen wir uns das transformierte Beispiel an.

1
function iterateNodeIterator (template, model) {
2
	var currentNode;
3
	var fragment = template.content.clone(true);
4
	var iterator = document.createNodeIterator(
5
	    fragment,
6
	    NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT,
7
	    acceptAllNodes,
8
	    false
9
	);
10
11
	while ((currentNode = iterator.nextNode()))
12
		changeNode(currentNode);
13
14
	return fragment;
15
}

Die DOM-Suche ist uns vollständig verborgen. Dies ist ein großer Vorteil. Wir fordern nur die gewünschten Knoten an und der Rest wird auf die effizienteste Weise in der Engine des Browsers erledigt. Auf der anderen Seite müssen wir jedoch noch Code zum Iterieren der Attribute bereitstellen.

Obwohl Attribute von der SHOW_ATTRIBUTE-Konstante abgedeckt werden, sind sie nicht als Kinder mit den Elementknoten verbunden. Stattdessen befinden sie sich in einer NamedNodeMap-Sammlung, die vom NodeIterator nicht in die Suche einbezogen wird. Wir können nur über Attribute iterieren, wenn wir die Iteration bei einem Attribut beginnen und uns auf Attribute beschränken.

Das vorherige Beispiel könnte auch die Änderung im bereitgestellten Filter aufrufen. Dies ist jedoch keine gute Vorgehensweise, da wir den Iterator möglicherweise auch für andere Zwecke verwenden möchten. Daher sollte der Iterator eigentlich nur eine schreibgeschützte Lösung präsentieren, die den Baum nicht mutiert.

Auch das Mutieren des Baumes wird vom NodeIterator nicht sehr gut unterstützt. Der Iterator kann man sich wie einen Cursor in einem Dokument vorstellen, der zwischen zwei (dem letzten und dem nächsten) Knoten platziert wird. Daher zeigt ein NodeIterator nicht auf einen beliebigen Knoten.

Den Baum gehen

Wir wollen über Knoten in einem Teilbaum iterieren. Eine andere Möglichkeit, die uns in den Sinn kommt, ist die Verwendung eines TreeWalker. Hier gehen wir den Baum entlang, wie der Name schon sagt. Wir geben einen Wurzelknoten und Elemente an, die in unserer Route berücksichtigt werden sollen, und dann verarbeiten wir. Das Interessante daran ist, dass ein TreeWalker viel mit einem NodeIterator gemeinsam hat. Wir sehen nicht nur bereits viele gemeinsame Eigenschaften, sondern verwendet auch denselben NodeFilter zum Einrichten von Einschränkungen.

In den meisten Szenarien ist der TreeWalker tatsächlich die bessere Wahl als der NodeIterator. Die API des NodeIterator ist für das, was sie liefert, aufgebläht. Der TreeWalker enthält noch mehr Methoden und Einstellungen, verwendet sie aber zumindest.

Der Hauptunterschied zwischen dem TreeWalker und dem NodeIterator besteht darin, dass erstere eine baumorientierte Ansicht der Knoten in einem Teilbaum darstellt und nicht die listenorientierte Ansicht des Iterators. Während der NodeIterator es uns ermöglicht, vorwärts oder rückwärts zu gehen, bietet uns ein TreeWalker auch die Möglichkeit, zum Elternteil eines Knotens, zu einem seiner Kinder oder zu einem Geschwisterknoten zu wechseln.

1
function iterateTreeWalker (template, model) {
2
	var fragment = template.content.clone(true);
3
	var walker = document.createTreeWalker(
4
	    fragment,
5
	    NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT,
6
	    acceptAllNodes,
7
	    false
8
	);
9
10
	while (walker.nextNode())
11
		changeNode(treeWalker.currentNode);
12
13
	return fragment;
14
}

Im Gegensatz zum NodeIterator zeigt der „TreeWalker“ direkt auf einen bestimmten Knoten im Baum. Wenn der Knoten, auf den verwiesen wird, verschoben wird, folgt der TreeWalker daher. Noch wichtiger ist, dass wir effektiv außerhalb des Dokumentbaums landen, wenn der Knoten, auf den verwiesen wird, aus dem Baum entfernt wird. Wenn wir den Ratschlägen für den NodeIterator folgen und den Baum beim Durchlaufen nicht mutieren, werden wir am Ende den gleichen Pfad haben.

Der TreeWalker scheint für unseren Zweck auch fast identisch mit dem NodeIterator zu sein. Es gibt Gründe, warum letztere nicht viel Aufmerksamkeit erlangen konnten. Trotzdem ist der TreeWalker auch nicht sehr bekannt. Vielleicht ist sein Anwendungsbereich einfach zu begrenzt, so dass wir keine Möglichkeit haben, Attribute erneut zu durchlaufen – insbesondere mit der dritten Option, die wir für die DOM-Baum-Iteration haben.

Bereichsauswahl

Schließlich gibt es noch ein drittes Konstrukt, das unter Umständen interessant sein kann. Wenn wir einen Bereich in einem 1-dimensionalen Array auswählen möchten, können wir dies leicht implementieren, indem wir einfach zwei Indizes verwenden: i für die anfängliche(linke) Grenze und f für die letzte (rechte) Grenze, die wir haben, [i, f].

Wenn wir das Array durch eine verkettete Liste ersetzen, können die beiden Indizes auch durch zwei konkrete Knoten [n_i, n_f] ersetzt werden. Der Vorteil dieser Wahl liegt im impliziten Update-Mechanismus. Wenn wir dazwischen Knoten einfügen, müssen wir die Grenzen unseres Bereichs nicht aktualisieren. Auch wenn die linke Grenze aus der verknüpften Liste entfernt wird, erhalten wir einen Bereich, der sich nach links erweitert hat, beispielsweise [0, n_f].

Jetzt haben wir kein 1-dimensionales Problem, sondern eine Baumstruktur. Die Auswahl eines Bereichs eines K-ary-Baums ist nicht so trivial. Wir könnten unseren eigenen Algorithmus entwickeln, aber ein DOM-Baum hat einige spezielle Probleme. Zum Beispiel haben wir Textknoten, die auch Gegenstand eines Bereichs sein können. In unserem DOM-Baum besteht das Sortiment aus vier Eigenschaften. Wir haben einen Startknoten, einen Endknoten und einen Offset für beide.

Es gibt auch Helfer, wie die Methoden selectNode oder selectNodeContents, die die richtigen Aufrufe von setStart und setEnd ausführen. Der Aufruf von selectNodeContents(node) entspricht beispielsweise dem Code-Snippet:

1
range.setStart(node, 0);
2
range.setEnd(node, node.childNodes.length);

Bereiche gehen über die reine programmatische Auswahl hinaus. Sie werden auch für die eigentliche visuelle Auswahl im Browser verwendet. Die Methode getSelection() des window-Kontexts liefert ein Selection-Objekt, das durch Aufrufen von getRangeAt(0) leicht in einen Range umgewandelt werden kann. Wenn nichts ausgewählt ist, schlägt die vorherige Anweisung fehl.

Betrachten wir ein einfaches Beispiel für eine Auswahl, die zu dem folgenden Bild führt.

DOM Selection

Hier beginnen wir im Textknoten des ersten Listenelements und enden am Ende des Textknotens eines starken Elements. Das folgende Bild veranschaulicht den abgedeckten Bereich aus Sicht des Quellcodes.

DOM Range Source

Interessant ist auch die Anzeige des DOM-Baums für die bereitgestellte Range-Instanz. Wir sehen, dass ein solches Spektrum eine ganze Reihe von Knoten abdecken kann, unabhängig von ihren Vorfahren oder Geschwistern.

DOM Range TreeDOM Range TreeDOM Range Tree

Durch das Extrahieren der ausgewählten Knoten erhalten wir ein DocumentFragment, das bei einem neuen Listenelement beginnt und nach dem starken Element endet.

DOM Range ExtractDOM Range ExtractDOM Range Extract

Die Extraktion ist eigentlich eine DOM-Mutationsaktion, d. h. sie ändert den vorhandenen Baum. Jetzt bleiben uns die folgenden zwei Punkte, genau wie wir es erwartet hatten.

DOM Range Extracted

Da Text Elemente und alles, was darin enthalten sein kann, enthalten kann, muss der Bereich auch diese Fälle abdecken. Auf den ersten Blick ist der Range seltsam konstruiert, da er sich immer um mindestens zwei Fälle kümmern muss: Text und Nicht-Text (meistens Elemente). Wie wir gesehen haben, gibt es jedoch einen guten Grund, diese beiden Fälle zu unterscheiden, da wir sonst keine Textfragmente auswählen könnten.

Dem Range-Objekt fehlen die Iterationsfähigkeiten, die wir zuvor kennengelernt haben. Stattdessen haben wir die Serialisierungs- und Exportfunktionen verbessert. Unser bisheriges Beispiel zu ändern ist daher zunächst umständlich. Dennoch sind wir durch die Einführung einiger neuer Methoden in der Lage, die Flexibilität eines Range mit verbesserter Iteration zu kombinieren.

1
Range.prototype.current = function () {
2
	if (this.started)
3
		return this.startContainer.childNodes[this.startOffset];
4
};
5
6
Range.prototype.next = function (types) {
7
	var s = this.startContainer;
8
	var o = this.startOffset;
9
	var n = this.current();
10
11
	if (n) {
12
		if (n.childNodes.length) {
13
			s = n;
14
			o = 0;
15
		} else if (o + 1 < s.childNodes.length) {
16
			o += 1;
17
		} else {
18
			do {			
19
				n = s.parentNode;
20
21
				if (s == this.endContainer)
22
					return false;
23
24
				o = Array.prototype.indexOf.call(n.childNodes, s) + 1;
25
				s = n;
26
			} while (o === s.childNodes.length);
27
		}
28
29
		this.setStart(s, o);
30
		n = this.current();
31
	} else if (!this.started) {
32
		this.started = true;
33
		n = this.current();
34
	}
35
36
	if (n && types && Array.isArray(types) && types.indexOf(n.nodeType) < 0)
37
		return this.next();
38
39
	return !!n;
40
};

Mit diesen beiden Methoden können wir den Range genau wie die Iteratoren zuvor verwenden. Im Moment können wir nur in eine Richtung gehen; Wir könnten jedoch problemlos Methoden implementieren, um die untergeordneten Elemente zu überspringen, direkt zum übergeordneten Element zu wechseln oder andere Bewegungen auszuführen.

1
function iterateRange (template, model) {
2
	var fragment = template.content.clone(true);
3
	var range = document.createRange();
4
    range.selectNodeContents(fragment);
5
6
	while (range.nextNode([Node.TEXT_NODE, Node.ELEMENT_NODE]))
7
		changeNode(range.current());
8
9
	range.detach();
10
	return fragment;
11
}

Auch wenn Range wie jedes andere JavaScript-Objekt Garbage-Collection ist, empfiehlt es sich dennoch, ihn mit der Funktion detach freizugeben. Einer der Gründe ist, dass alle Range-Instanzen tatsächlich im document gehalten werden, wo sie im Falle von DOM-Mutationen aktualisiert werden.

Es ist von Vorteil, unsere eigenen Iteratormethoden für den Range zu definieren. Die Offsets werden automatisch aktualisiert und wir haben Hilfsmöglichkeiten, wie das Klonen der aktuellen Auswahl als DocumentFragment, das Extrahieren oder Löschen der ausgewählten Knoten. Auch können wir die API für unsere eigenen Bedürfnisse entwerfen.

Abschluss

Das Iterieren durch den DOM-Baum ist ein interessantes Thema für jeden, der über DOM-Manipulation und effizientes Abrufen von Knoten nachdenkt. Für die meisten Anwendungsfälle gibt es bereits eine entsprechende API. Wollen wir eine einfache Iteration? Wollen wir einen Bereich auswählen? Haben wir Lust auf den Baum? Für jede Methode gibt es Vor- und Nachteile. Wenn wir wissen, was wir wollen, können wir die richtige Wahl treffen.

Leider sind Baumstrukturen nicht so einfach wie eindimensionale Arrays. Sie können auf eindimensionale Arrays abgebildet werden, aber die Abbildung folgt demselben Algorithmus wie das Iterieren über ihre Struktur. Wenn wir eine der bereitgestellten Strukturen verwenden, haben wir Zugriff auf Methoden, die diesen Algorithmen bereits folgen. Daher erhalten wir eine bequeme Möglichkeit, einige Iterationen über Knoten in einem K-ary-Baum durchzuführen. Wir sparen auch etwas Leistung, indem wir weniger DOM-Operationen ausführen.

Verweise

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.