web-dev-qa-db-fra.com

Moyen le plus rapide de trouver l'index d'un nœud enfant dans parent

Je veux trouver l'index de la div enfant qui a l'ID 'whereami'.

<div id="parent">
   <div></div>
   <div></div>
   <div id="whereami"></div>
   <div></div>
</div>

Actuellement, j'utilise cette fonction pour trouver l'index de l'enfant.

function findRow(node){
    var i=1;
    while(node.previousSibling){
        node = node.previousSibling;
        if(node.nodeType === 1){
            i++;
        }
    }
    return i; //Returns 3
}

var node = document.getElementById('whereami'); //div node to find
var index = findRow(node);

violon: http://jsfiddle.net/grantk/F7JpH/2/

Le problème
Lorsqu'il y a des milliers de nœuds div, la boucle while doit traverser chaque div pour les compter. Ce qui peut prendre un moment.

Y a-t-il un moyen plus rapide de s'y attaquer?

* Notez que l'id va changer de nœud divs, il devra donc pouvoir recalculer.

31
gkiely

Par curiosité, j'ai lancé votre code à la fois contre .index() et mon code ci-dessous de jQuery:

function findRow3(node)
{
    var i = 1;
    while (node = node.previousSibling) {
        if (node.nodeType === 1) { ++i }
    }
    return i;
}

Aller aux résultats jsperf

Il s’avère que jQuery est environ 50% plus lent que votre implémentation (sur Chrome/Mac) et que le mien l’a dépassé de 1%.

Modifier

Ne pouvant pas laisser passer celui-ci, j'ai donc ajouté deux approches supplémentaires:

Utilisation de Array.indexOf

[].indexOf.call(node.parentNode.children, node);

Amélioration de mon précédent code expérimental, comme indiqué dans réponse de HBP , la variable DOMNodeList est traitée comme un tableau et utilise Array.indexOf() pour déterminer la position dans son .parentNode.children qui sont tous des éléments. Ma première tentative utilisait .parentNode.childNodes, mais cela donne des résultats incorrects à cause des nœuds de texte.

Utilisation de previousElementSibling

Inspirés par user1689607's answer , les navigateurs récents possèdent une propriété autre que .previousSibling appelée .previousElementSibling, qui effectue les deux instructions d'origine en une. IE <= 8 ne possède pas cette propriété, mais .previousSibling agit déjà en tant que tel. Par conséquent, une détection de fonctionnalité devrait fonctionner.

(function() {
    // feature detection
    // use previousElementSibling where available, IE <=8 can safely use previousSibling
    var prop = document.body.previousElementSibling ? 'previousElementSibling' : 'previousSibling';

    getElementIndex = function(node) {
        var i = 1;
        while (node = node[prop]) { ++i }
        return i;
    }

Conclusion 

L'utilisation de Array.indexOf() n'est pas prise en charge sur les navigateurs IE <= 8 et l'émulation n'est tout simplement pas assez rapide. Cependant, cela donne une amélioration de 20% des performances.

L'utilisation de la détection de fonctionnalités et de .previousElementSibling produit une amélioration 7x (sur Chrome), je n'ai pas encore testé sur IE8.

37
Ja͢ck

En cooptant ArrayindexOf, vous pouvez utiliser:

  var wmi = document.getElementById ('whereami');
  index = [].indexOf.call (wmi.parentNode.children, wmi);

[Testé sur le navigateur Chrome uniquement]

5
HBP

J'ai ajouté deux tests au jsPerf test . Les deux utilisent previousElementSibling, mais le second inclut un code de compatibilité pour IE8 et les versions antérieures.

Les deux fonctionnent très bien dans les navigateurs modernes (qui est la plupart des navigateurs utilisés de nos jours), mais prendra un petit succès dans les anciens navigateurs.


Voici le premier qui n'inclut pas le correctif de compatibilité. Cela fonctionnera dans IE9 et supérieur, ainsi que dans la quasi-totalité de Firefox, Chrome et Safari.

function findRow6(node) {
    var i = 1;
    while (node = node.previousElementSibling)
        ++i;
    return i;
}

Voici la version avec le correctif de compatibilité.

function findRow7(node) {
    var i = 1,
        prev;
    while (true)
        if (prev = node.previousElementSibling) {
            node = prev;
            ++i;
        } else if (node = node.previousSibling) {
            if (node.nodeType === 1) {
                ++i;
            }
        } else break;
    return i;
}

Comme il saisit automatiquement les frères et soeurs de l'élément, aucun test n'est requis pour nodeType et la boucle est globalement plus courte. Ceci explique la forte augmentation des performances.


J'ai également ajouté une dernière version qui boucle le .children et compare le node à chacun.

Ce n'est pas aussi rapide que les versions previousElementSibling, mais reste plus rapide que les autres (du moins dans Firefox).

function findRow8(node) {
    var children = node.parentNode.children,
        i = 0,
        len = children.length;
    for( ; i < len && children[i] !== node; i++)
        ; // <-- empty statement

    return i === len ? -1 : i;
}


Pour revenir à la version previousElementSibling, voici un Tweak qui risque d’accroître légèrement les performances.

function findRow9(node) {
    var i = 1,
        prev = node.previousElementSibling;

    if (prev) {
        do ++i;
        while (prev = prev.previousElementSibling);
    } else {
        while (node = node.previousSibling) {
            if (node.nodeType === 1) {
                ++i;
            }
        }
    }
    return i;
}

Je ne l'ai pas testé dans le jsPerf, mais le diviser en deux boucles différentes basées sur la présence d'un previouselementSibling ne ferait qu'aider à mon avis.

Peut-être que je l'ajouterai un peu.

Je suis allé de l'avant et l'ajouté au test lié en haut de cette réponse. Cela aide un peu, alors je pense que cela en vaut probablement la peine.

3
I Hate Lazy

De manière générale, une petite différence de performance a un effet négligeable sauf si le code est exécuté en boucle. Avoir à exécuter le code une fois au lieu de chaque fois sera nettement plus rapide.

Faites quelque chose comme ça une fois:

var par = document.getElementById('parent');
var childs = par.getElementsByTagName('*');
for (var i=0, len = childs.length;i < len;i++){
  childs[i].index = i;
}

Trouver ensuite l'index est aussi simple que:

document.getElementById('findme').index

Il semble que tout ce que vous faites pourrait bénéficier d’une relation plus claire entre le DOM et le javascript. Pensez à l’apprentissage de Backbone.js, une petite bibliothèque MVC javascript qui facilite beaucoup le contrôle des applications Web.

edit: J'ai supprimé le jQuery que j'ai utilisé. Normalement, j'évite de l'utiliser, mais comme il convient plutôt à SO, j'ai donc supposé qu'il serait utilisé de toute façon. Ici, vous pouvez voir la différence évidente: http://jsperf.com/sibling-index/8

1
mowwwalker

Depuis que vous utilisez jQuery. index devrait faire l'affaire

jQuery('#whereami').index()

mais comment allez-vous utiliser l'index plus tard?

1
fedmich

Essaye ça:

function findRow(node) {
    var i = 1;
    while ((node = node.previousSibling) != null) {
        if (node.nodeType === 1) i++;
    }
    return i; //Returns 3
}
1
palaѕн

Je pars du principe que pour un élément où tous ses enfants sont classés par ordre dans le document, le moyen le plus rapide consiste à effectuer une recherche binaire en comparant les positions des éléments dans les documents. Cependant, comme introduit dans la conclusion, l'hypothèse est rejetée. Plus vous avez d'éléments, plus le potentiel de performance est élevé. Par exemple, si vous avez 256 éléments, alors (de manière optimale), vous ne devriez en vérifier que 16! Pour 65536, seulement 256! La performance passe à la puissance de 2! Voir plus de chiffres/statistiques. Visitez Wikipedia

(function(constructor){
   'use strict';
    Object.defineProperty(constructor.prototype, 'parentIndex', {
      get: function() {
        var searchParent = this.parentElement;
        if (!searchParent) return -1;
        var searchArray = searchParent.children,
            thisOffset = this.offsetTop,
            stop = searchArray.length,
            p = 0,
            delta = 0;

        while (searchArray[p] !== this) {
            if (searchArray[p] > this)
                stop = p + 1, p -= delta;
            delta = (stop - p) >>> 1;
            p += delta;
        }

        return p;
      }
    });
})(window.Element || Node);

Ensuite, vous l'utilisez en obtenant la propriété 'parentIndex' de n'importe quel élément. Par exemple, consultez la démo suivante.

(function(constructor){
   'use strict';
    Object.defineProperty(constructor.prototype, 'parentIndex', {
      get: function() {
        var searchParent = this.parentNode;
        if (searchParent === null) return -1;
        var childElements = searchParent.children,
            lo = -1, mi, hi = childElements.length;
        while (1 + lo !== hi) {
            mi = (hi + lo) >> 1;
            if (!(this.compareDocumentPosition(childElements[mi]) & 0x2)) {
                hi = mi;
                continue;
            }
            lo = mi;
        }
        return childElements[hi] === this ? hi : -1;
      }
    });
})(window.Element || Node);

output.textContent = document.body.parentIndex;
output2.textContent = document.documentElement.parentIndex;
Body parentIndex is <b id="output"></b><br />
documentElements parentIndex is <b id="output2"></b>

Limitations

  • Cette implémentation de la solution ne fonctionnera pas dans IE8 et inférieur.

Recherche binaire vs linéaire sur 200 000 éléments (il est possible que certains navigateurs mobiles se bloquent, ATTENTION!):

  • Dans ce test, nous verrons combien de temps il faut à une recherche linéaire pour trouver l'élément intermédiaire VS à une recherche binaire. Pourquoi l'élément central? Comme il se trouve à l'emplacement moyen de tous les autres emplacements, il représente donc le mieux possible tous les emplacements possibles.

Recherche binaire

(function(constructor){
   'use strict';
    Object.defineProperty(constructor.prototype, 'parentIndexBinarySearch', {
      get: function() {
        var searchParent = this.parentNode;
        if (searchParent === null) return -1;
        var childElements = searchParent.children,
            lo = -1, mi, hi = childElements.length;
        while (1 + lo !== hi) {
            mi = (hi + lo) >> 1;
            if (!(this.compareDocumentPosition(childElements[mi]) & 0x2)) {
                hi = mi;
                continue;
            }
            lo = mi;
        }
        return childElements[hi] === this ? hi : -1;
      }
    });
})(window.Element || Node);
test.innerHTML = '<div> </div> '.repeat(200e+3);
// give it some time to think:
requestAnimationFrame(function(){
  var child=test.children.item(99.9e+3);
  var start=performance.now(), end=Math.round(Math.random());
  for (var i=200 + end; i-- !== end; )
    console.assert( test.children.item(
        Math.round(99.9e+3+i+Math.random())).parentIndexBinarySearch );
  var end=performance.now();
  setTimeout(function(){
    output.textContent = 'It took the binary search ' + ((end-start)*10).toFixed(2) + 'ms to find the 999 thousandth to 101 thousandth children in an element with 200 thousand children.';
    test.remove();
    test = null; // free up reference
  }, 125);
}, 125);
<output id=output> </output><br />
<div id=test style=visibility:hidden;white-space:pre></div>

Recherche linéaire vers l'arrière (`lastIndexOf`)

(function(t){"use strict";var e=Array.prototype.lastIndexOf;Object.defineProperty(t.prototype,"parentIndexLinearSearch",{get:function(){return e.call(t,this)}})})(window.Element||Node);
test.innerHTML = '<div> </div> '.repeat(200e+3);
// give it some time to think:
requestAnimationFrame(function(){
  var child=test.children.item(99e+3);
  var start=performance.now(), end=Math.round(Math.random());
  for (var i=2000 + end; i-- !== end; )
    console.assert( test.children.item(
        Math.round(99e+3+i+Math.random())).parentIndexLinearSearch );
  var end=performance.now();
  setTimeout(function(){
    output.textContent = 'It took the backwards linear search ' + (end-start).toFixed(2) + 'ms to find the 999 thousandth to 101 thousandth children in an element with 200 thousand children.';
    test.remove();
    test = null; // free up reference
  }, 125);
});
<output id=output> </output><br />
<div id=test style=visibility:hidden;white-space:pre></div>

Forwards (`indexOf`) Recherche linéaire

(function(t){"use strict";var e=Array.prototype.indexOf;Object.defineProperty(t.prototype,"parentIndexLinearSearch",{get:function(){return e.call(t,this)}})})(window.Element||Node);
test.innerHTML = '<div> </div> '.repeat(200e+3);
// give it some time to think:
requestAnimationFrame(function(){
  var child=test.children.item(99e+3);
  var start=performance.now(), end=Math.round(Math.random());
  for (var i=2000 + end; i-- !== end; )
    console.assert( test.children.item(
        Math.round(99e+3+i+Math.random())).parentIndexLinearSearch );
  var end=performance.now();
  setTimeout(function(){
    output.textContent = 'It took the forwards linear search ' + (end-start).toFixed(2) + 'ms to find the 999 thousandth to 101 thousandth children in an element with 200 thousand children.';
    test.remove();
    test = null; // free up reference
  }, 125);
});
<output id=output> </output><br />
<div id=test style=visibility:hidden;white-space:pre></div>

PreviousElementSibling Counter Search

Compte le nombre de PreviousElementSiblings pour obtenir le parentIndex. 

(function(constructor){
   'use strict';
    Object.defineProperty(constructor.prototype, 'parentIndexSiblingSearch', {
      get: function() {
        var i = 0, cur = this;
        do {
            cur = cur.previousElementSibling;
            ++i;
        } while (cur !== null)
        return i; //Returns 3
      }
    });
})(window.Element || Node);
test.innerHTML = '<div> </div> '.repeat(200e+3);
// give it some time to think:
requestAnimationFrame(function(){
  var child=test.children.item(99.95e+3);
  var start=performance.now(), end=Math.round(Math.random());
  for (var i=100 + end; i-- !== end; )
    console.assert( test.children.item(
        Math.round(99.95e+3+i+Math.random())).parentIndexSiblingSearch );
  var end=performance.now();
  setTimeout(function(){
    output.textContent = 'It took the PreviousElementSibling search ' + ((end-start)*20).toFixed(2) + 'ms to find the 999 thousandth to 101 thousandth children in an element with 200 thousand children.';
    test.remove();
    test = null; // free up reference
  }, 125);
});
<output id=output> </output><br />
<div id=test style=visibility:hidden;white-space:pre></div>

Pas de recherche

Pour l'analyse comparative, quel serait le résultat du test si le navigateur optimisait la recherche.

test.innerHTML = '<div> </div> '.repeat(200e+3);
// give it some time to think:
requestAnimationFrame(function(){
  var start=performance.now(), end=Math.round(Math.random());
  for (var i=2000 + end; i-- !== end; )
    console.assert( true );
  var end=performance.now();
  setTimeout(function(){
    output.textContent = 'It took the no search ' + (end-start).toFixed(2) + 'ms to find the 999 thousandth to 101 thousandth children in an element with 200 thousand children.';
    test.remove();
    test = null; // free up reference
  }, 125);
});
<output id=output> </output><br />
<div id=test style=visibility:hidden></div>

La conculsion

Cependant, après avoir consulté les résultats dans Chrome, les résultats sont à l'opposé de ceux attendus. La recherche linéaire vers l'avant plus bête était étonnante de 187 ms, 3 850%, plus rapide que la recherche binaire. Évidemment, Chrome a magiquement déjoué et optimisé le console.assert par magie, ou utilise (de façon plus optimiste) en interne un système d'indexation numérique pour le DOM et ce système d'indexation interne est exposé via les optimisations appliquées à Array.prototype.indexOf lorsqu'il est utilisé sur un objet HTMLCollection.

1
Jack Giffin