L'un des concepts les plus importants du DOM est la traversée des arbres. Depuis que l'informatique est devenue son propre domaine d'étude, des décennies de recherche ont été consacrées aux structures de données et aux algorithmes. Une des structures les plus utilisées est un arbre. Les arbres sont partout. Une version très rudimentaire, pourtant utile et souvent utilisée est un arbre binaire. Un tournoi peut être représenté sous la forme d'un arbre binaire. L'arbre DOM n'est cependant pas binaire. Au lieu de cela, c'est un arbre K-aire. Chaque nœud peut avoir zéro à N sous-noeuds, appelés childNodes
.
Une arborescence DOM héberge un large éventail de types de nœuds possibles. Il peut y avoir Texte
, Élément
, Commentaire
et d'autres spéciaux, tels que TraitementInstruction
ou Type de document
, parmi tant d'autres. La plupart d'entre eux n'auront aucun childNodes
par définition. Ce sont des points d'extrémité et ne portent qu'une seule information. Par exemple, un Commentaire
noeud ne porte que la chaîne de commentaire spécifiée. UNE Texte
noeud n'est disponible que pour stocker une chaîne de contenu.
le Élément
noeud héberge d'autres noeuds. On peut récursivement descendre d'éléments en éléments pour parcourir tous les nœuds disponibles dans le système..
Un exemple qui concerne également l’article précédent concernant la element est en train de remplir une structure de sous-arbre DOM. Une sous-arborescence est une partie de l'arborescence, qui commence à un élément spécifié. Nous appelons l'élément spécifié la racine du sous-arbre. Si on prend le
élément d’un arbre en tant que racine du sous-arbre, celui-ci serait presque identique à l’arbre réel, qui commence au début.
document
, c'est-à-dire un niveau en dessous du documentElement
.
Pour remplir la structure de sous-arbre, nous devons effectuer une itération sur tous les enfants de la racine de sous-arbre. Sur chaque nœud, nous devons vérifier le type exact de nœud, puis procéder de la manière appropriée. Par exemple, chaque élément doit à nouveau être considéré comme une racine de sous-arbre. Les nœuds de texte, en revanche, doivent être évalués avec plus de soin. Peut-être voudrons-nous aussi vérifier les nœuds de commentaires pour les directives spéciales. En outre, les attributs des éléments doivent être considérés aussi bien.
Pour le scénario, nous utilisons une méthode appelée applyModel
pour remplir des chaînes basées sur un modèle avec les valeurs d'un modèle. La méthode se présente comme suit et pourrait bien sûr être optimisée. Néanmoins, pour nos besoins, il est certainement suffisant.
"fonction javascript applyModel (modèle, données) var rx = new RegExp ('\ \ s(. +?) \ s\', 'g'); var group = rx.exec (data);
while (groupe) nom var = groupe [1]; var value = "; eval ('avec (modèle) valeur =' + nom + ';'); data = data.replace (groupe [0], valeur); group = rx.exec (données); retour Les données; "
Voyons une implémentation pour le scénario décrit, qui utilise le applyModel
méthode à diverses occasions. Cela prend une instance d'un modèle
élément et un objet appelé modèle
retourner un nouveau DocumentFragment
. Le nouveau fragment utilise les données du modèle pour modifier toutes les valeurs de X
au résultat de l'évaluation de l'expression X
en utilisant l'objet fourni.
"fonction javascript iterateClassic (template, modèle) var fragment = template.content.clone (true); var allNodes = findAllNodes (fragment);
allNodes.forEach (changeNode); retourner le fragment; "
Le code précédent utilise une fonction findAllNodes
, qui prend un noeud et stocke tous ses enfants dans un tableau. La fonction est ensuite appelée de manière récursive sur chaque enfant. À la fin, tous les résultats sont fusionnés dans un tableau unique de l’ensemble du sous-arbre, c’est-à-dire que nous transformons la structure arborescente en un tableau à une dimension..
L'extrait suivant montre un exemple de mise en oeuvre pour l'algorithme décrit..
"fonction javascript findAllNodes (childNodes) var nodes = [];
if (childNodes && childNodes.length> 0) pour (var i = 0, length = childNodes.length; i < length; i++) nodes.push(childNodes[i]); nodes = nodes.concat(findAllNodes(childNodes[i].childNodes)); return nodes; "
La fonction permettant de changer chaque noeud du tableau est présentée ci-dessous. La fonction effectue certaines manipulations en fonction du type de nœud. Nous nous soucions seulement des attributs et des nœuds de texte.
fonction javascript changeNode (node) switch (node.nodeType) case Node.TEXT_NODE: node.text.data = applyModel (modèle, node.text.data); Pause; case Node.ELEMENT_NODE: Array.prototype.forEach.call (node.attributes, fonction (attribut) attribut.value = applyModel (modèle, attribut.value);); Pause;
Même si le code est facile à comprendre, il n’est pas très joli. Nous avons pas mal de problèmes de performances, d’autant plus que nous avons malheureusement beaucoup d’opérations DOM nécessaires. Cela peut être fait plus efficacement en utilisant l'un des helpers de l'arborescence DOM. Notez que le findAllNodes
méthode retourne un tableau avec tous les nœuds, pas seulement tous Élément
instances dans la sous-arborescence. Si ce dernier nous intéresse, nous pouvons simplement utiliser un querySelectorAll ('*')
call, qui effectue l'itération pour nous.
Une solution qui se présente immédiatement consiste à utiliser un NodeIterator
. UNE NodeIterator
itère sur les nœuds. Cela correspond presque parfaitement à nos critères. Nous pouvons créer un nouveau NodeIterator
en utilisant le createNodeIterator
méthode du document
objet. Il y a trois paramètres cruciaux:
acceptNode
pour le filtrage personnalisé.Bien que le premier argument ne soit qu'un simple nœud DOM, les deux autres utilisent des constantes spéciales. Par exemple, si tous les nœuds doivent être affichés, nous devons passer -1
en tant que filtre. Sinon, nous pouvons utiliser NodeFilter.SHOW_ALL
. Nous pourrions combiner plusieurs filtres de plusieurs manières. Par exemple, la combinaison de tous les commentaires et de tous les éléments peut être exprimée avec NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_ELEMENT
.
Le troisième argument est un objet qui peut sembler aussi primitif que l'extrait de code suivant. Même si l'objet qui enveloppe la fonction semble être redondant, il a été spécifié de cette façon. Certains navigateurs, par exemple Mozilla Firefox, nous donne la possibilité de réduire l'objet à une seule fonction.
javascript var acceptAllNodes = acceptNode: fonction (noeud) return NodeFilter.FILTER_ACCEPT; ;
Nous acceptons ici tous les nœuds transmis. De plus, nous avons la possibilité de rejeter un nœud (et ses enfants) avec le FILTER_REJECT
option. Si nous voulons juste ignorer le noeud, mais sommes toujours intéressés par ses enfants, nous pouvons utiliser le FILTER_SKIP
constant.
Mise en œuvre de notre ancien exemple en utilisant le NodeIterator
est assez simple. Nous créons un nouvel itérateur en utilisant la méthode constructeur dans le document
. Ensuite, nous utilisons le nextNode
méthode pour parcourir tous les nœuds.
Regardons l'exemple transformé.
"fonction javascript iterateNodeIterator (template, modèle) var currentNode; var fragment = template.content.clone (true); var itérateur = document.createNodeIterator (fragment, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT, acceptAllNodes, false);
while ((currentNode = iterator.nextNode ())) changeNode (currentNode); retourner le fragment; "
La recherche dans le DOM nous est complètement cachée. C'est un grand avantage. Nous ne demandons que les nœuds que nous désirons, et le reste est effectué de la manière la plus efficace dans le moteur du navigateur. Cependant, de l’autre côté, nous devons toujours fournir du code pour itérer les attributs..
Même si les attributs sont couverts par le SHOW_ATTRIBUTE
constante, ils ne sont pas liés aux nœuds d'élément en tant qu'enfants. Au lieu de cela, ils vivent dans un NamedNodeMap
collection, qui ne sera pas incluse dans la recherche par le NodeIterator
. Nous ne pouvons itérer sur les attributs que si nous commençons l'itération sur un attribut, en nous limitant à ceux-ci..
L'exemple précédent pourrait également invoquer la modification du filtre fourni. Ce n'est toutefois pas une bonne pratique, car nous pourrions également vouloir utiliser l'itérateur à d'autres fins. Par conséquent, l'itérateur devrait simplement présenter une solution en lecture seule, qui ne mute pas l'arbre.
La mutation de l’arbre n’est pas non plus très bien supportée par le NodeIterator
. L'itérateur peut être considéré comme un curseur dans un document, placé entre deux nœuds (le dernier et le suivant). Donc un NodeIterator
ne pointe pas vers un noeud quelconque.
Nous voulons parcourir les nœuds d'un sous-arbre. Une autre option qui nous vient à l’esprit est d’utiliser un TreeWalker
. Ici nous marchons dans l’arbre comme son nom l’indique déjà. Nous spécifions un nœud racine et des éléments à prendre en compte dans notre route, puis nous procédons. La partie intéressante est que TreeWalker
a beaucoup en commun avec un NodeIterator
. Non seulement nous voyons déjà beaucoup de propriétés partagées, il utilise également le même NodeFilter
mettre en place des contraintes.
Dans la plupart des scénarios, le TreeWalker
est en fait un meilleur choix que le NodeIterator
. L'API du NodeIterator
est gonflé pour ce qu'il offre. le TreeWalker
contient encore plus de méthodes et de paramètres, mais les utilise au moins.
La principale différence entre le TreeWalker
et le NodeIterator
est que le premier présente une vue arborescente des nœuds dans un sous-arbre, plutôt que la vue orientée liste de l'itérateur. Tandis que le NodeIterator
nous permet d'avancer ou de reculer, un TreeWalker
nous donne également la possibilité de passer au parent d'un nœud, à l'un de ses enfants ou à un frère.
"fonction javascript iterateTreeWalker (template, modèle) var fragment = template.content.clone (true); var walker = document.createTreeWalker (fragment, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT, acceptAllNodes, false);
while (walker.nextNode ()) changeNode (treeWalker.currentNode); retourner le fragment; "
Contrairement à la NodeIterator
, TreeWalker pointe directement sur un nœud spécifique de l'arborescence. Si le noeud pointé est déplacé, le TreeWalker
suivra donc. Plus important encore, si le nœud désigné est supprimé de l'arborescence, nous nous retrouverons effectivement en dehors de l'arborescence du document. Si nous suivons les conseils pour la NodeIterator
et ne mute pas l'arbre pendant le parcours, nous nous retrouverons avec le même chemin.
le TreeWalker
semble également être presque identique à la NodeIterator
pour notre but. Il y a des raisons pour lesquelles ce dernier n'a pas pu attirer beaucoup d'attention. Néanmoins le TreeWalker
n'est pas très connu non plus. Peut-être que son domaine d'utilisation est trop limité, ce qui ne nous permet plus de traverser les attributs, en particulier avec la troisième option de l'itération de l'arbre DOM..
Enfin, il existe une troisième construction qui peut être intéressante dans certaines circonstances. Si nous voulons sélectionner une plage dans un tableau à 1 dimension, nous pouvons facilement l'implémenter en utilisant simplement deux index: je
pour la limite initiale (gauche) et F
pour la limite finale (droite), nous avons, [si]
.
Si nous remplaçons le tableau par une liste chaînée, les deux index peuvent également être remplacés par deux nœuds concrets, [n_i, n_f]
. L'avantage de ce choix réside dans le mécanisme de mise à jour implicite. Si nous insérons des noeuds entre les deux, nous n'avons pas à mettre à jour les limites de notre plage. De même, si la limite gauche est supprimée de la liste liée, nous obtenons une plage étendue à gauche, telle que [0, n_f]
.
Maintenant, nous n’avons pas un problème à une dimension, mais une structure arborescente. La sélection d'une plage d'un arbre K-aire n'est pas si simple. Nous pourrions créer notre propre algorithme, mais un arbre DOM pose des problèmes particuliers. Par exemple, nous avons des nœuds de texte, qui peuvent également être soumis à une plage. Dans notre arbre DOM, la plage se compose de quatre propriétés. Nous avons un nœud de début, un nœud d'extrémité et un décalage pour les deux.
Il y a aussi des aides, comme le selectNode
ou selectNodeContents
méthodes, qui effectuent les bons appels de set start
et setEnd
. Par exemple, invoquer selectNodeContents (noeud)
est équivalent à l'extrait de code:
javascript range.setStart (node, 0); range.setEnd (node, node.childNodes.length);
Les gammes vont au-delà de la sélection purement programmatique. Ils sont également utilisés pour la sélection visuelle réelle dans le navigateur. le getSelection ()
méthode du la fenêtre
contexte donne un Sélection
objet, qui peut être facilement transformé en un Intervalle
en appelant getRangeAt (0)
. Si rien n'est sélectionné, l'instruction précédente échoue.
Prenons un exemple simple pour une sélection qui donne l'image suivante.
Ici, nous commençons dans le nœud de texte du premier élément de la liste et finissons à la fin du nœud de texte d'un élément fort. L'image suivante illustre la plage couverte du point de vue du code source.
Affichage de l’arborescence DOM pour le Intervalle
exemple est également intéressant. Nous voyons qu'une telle gamme est capable de couvrir toute une variété de nœuds indépendamment de leurs ancêtres ou frères..
Extraire les nœuds sélectionnés nous donne une DocumentFragment
, qui commence à un nouvel élément de la liste et se termine après l'élément fort.
L’extraction est en réalité une action de mutation du DOM, c’est-à-dire qu’elle modifiera l’arborescence existante. Il ne nous reste maintenant que les deux éléments suivants, exactement comme nous l’attendions..
Étant donné que le texte peut inclure des éléments et tout ce qu'ils contiennent, la plage doit également couvrir ces cas. À première vue le Intervalle
est étrangement conçu, car il doit toujours tenir compte d'au moins deux cas: texte et non-texte (principalement des éléments). Cependant, comme nous l'avons vu, il existe une bonne raison de distinguer ces deux cas, sinon nous ne pourrions pas sélectionner de fragments de texte..
le Intervalle
object manque les capacités d'itération que nous avons expérimentées précédemment. Au lieu de cela, nous avons amélioré les capacités de sérialisation et d'exportation. Changer notre ancien exemple est donc lourd au début. Néanmoins, en introduisant quelques nouvelles méthodes, nous sommes en mesure de combiner la flexibilité d’une Intervalle
avec itération améliorée.
"javascript Range.prototype.current = function () if (this.started) renvoie this.startContainer.childNodes [this.startOffset];;
Range.prototype.next = function (types) var s = this.startContainer; var o = this.startOffset; var n = this.current ();
if (n) if (n.childNodes.length) s = n; o = 0; sinon si (o + 1 < s.childNodes.length) o += 1; else do n = s.parentNode; if (s == this.endContainer) return false; o = Array.prototype.indexOf.call(n.childNodes, s) + 1; s = n; while (o === s.childNodes.length); this.setStart(s, o); n = this.current(); else if (!this.started) this.started = true; n = this.current(); if (n && types && Array.isArray(types) && types.indexOf(n.nodeType) < 0) return this.next(); return !!n; ;"
Ces deux méthodes nous permettent d’utiliser le Intervalle
tout comme les itérateurs avant. Pour le moment, nous ne pouvons aller que dans une direction; Cependant, nous pourrions facilement mettre en œuvre des méthodes pour ignorer les enfants, aller directement chez le parent ou effectuer tout autre déplacement..
"fonction javascript iterateRange (template, modèle) var fragment = template.content.clone (true); var range = document.createRange (); range.selectNodeContents (fragment);
while (range.nextNode ([Node.TEXT_NODE, Node.ELEMENT_NODE])) changeNode (range.current ()); range.detach (); retourner le fragment; "
Même si le Intervalle
les ordures sont collectées comme tout autre objet JavaScript, il est toujours bon de le libérer en utilisant le détacher
une fonction. Une des raisons est que tous Intervalle
les cas sont effectivement conservés dans le document
, où ils sont mis à jour en cas de mutations DOM.
Définir nos propres méthodes d'itérateur sur le Intervalle
est bénéfique. Les décalages sont automatiquement mis à jour et nous avons des possibilités auxiliaires, telles que le clonage de la sélection actuelle en tant que DocumentFragment
, extraire ou supprimer les nœuds sélectionnés. Aussi, nous pouvons concevoir l'API pour nos propres besoins.
Itérer dans l'arborescence DOM est un sujet intéressant pour quiconque pense à la manipulation du DOM et à la récupération efficace des noeuds. Dans la plupart des cas d'utilisation, il existe déjà une API correspondante. Voulons-nous une simple itération? Voulons-nous choisir une plage? Sommes-nous désireux de marcher dans l'arbre? Il existe des avantages et des inconvénients pour chaque méthode. Si nous savons ce que nous voulons, nous pouvons faire le bon choix.
Malheureusement, les structures arborescentes ne sont pas aussi simples que des tableaux à une dimension. Ils peuvent être mappés sur des tableaux unidimensionnels, mais le mappage suit le même algorithme que celui utilisé pour itérer sur sa structure. Si nous utilisons l'une des structures fournies, nous avons accès aux méthodes qui suivent déjà ces algorithmes. Par conséquent, nous obtenons un moyen pratique d'effectuer une itération sur les nœuds d'un arbre K-aire. Nous économisons également certaines performances en effectuant moins d'opérations DOM..