Pobierz indeks węzła podrzędnego

125

Czy w prostym javascript (tj. Bez rozszerzeń takich jak jQuery itp.) Istnieje sposób na określenie indeksu węzła podrzędnego w jego węźle nadrzędnym bez iteracji i porównywania wszystkich węzłów potomnych?

Na przykład,

var child = document.getElementById('my_element');
var parent = child.parentNode;
var childNodes = parent.childNodes;
var count = childNodes.length;
var child_index;
for (var i = 0; i < count; ++i) {
  if (child === childNodes[i]) {
    child_index = i;
    break;
  }
}

Czy istnieje lepszy sposób określenia indeksu dziecka?

Wszyscy pracownicy są niezbędni
źródło
2
Przepraszam, czy jestem kompletnym głupkiem? Jest tu wiele pozornie wyuczonych odpowiedzi, ale aby uzyskać wszystkie węzły potomne, nie musisz robić parent.childNodes, a nie parent.children?. Ta ostatnia wymienia tylko Elements, wyłączając poszczególne Textwęzły ... Część odpowiedzi, np. Używanie previousSibling, opiera się na wykorzystaniu wszystkich węzłów potomnych, podczas gdy inne dotyczą tylko dzieci, które są Element... (!)
mike gryzoń
@mikerodent Nie pamiętam, jaki był mój cel, kiedy zadawałem to pytanie, ale jest to kluczowy szczegół, którego nie byłem świadomy. Jeśli nie jesteś ostrożny, .childNodeszdecydowanie powinien być używany zamiast .children. Jak wskazałeś, 2 najczęściej opublikowane odpowiedzi dadzą różne wyniki.
Wszyscy pracownicy są niezbędni
Planując tysiące wyszukiwań w ponad 1000 węzłach, dołącz informacje do węzła (np. Przez child.dataset). Celem byłoby przekonwertowanie algorytmu O (n) lub O (n ^ 2) na algorytm O (1). Wadą jest to, że jeśli węzły są regularnie dodawane i usuwane, powiązane informacje o pozycji dołączone do węzłów również będą musiały być aktualizowane, co może nie powodować żadnego wzrostu wydajności. Sporadyczna iteracja nie jest wielkim problemem (np. Moduł obsługi kliknięć), ale powtarzająca się iteracja jest problematyczna (np. Przesunięcie myszy).
CubicleSoft

Odpowiedzi:

122

możesz użyć tej previousSiblingwłaściwości, aby przejść wstecz przez rodzeństwo, aż wrócisz nulli policzyć, ile rodzeństwa spotkałeś:

var i = 0;
while( (child = child.previousSibling) != null ) 
  i++;
//at the end i will contain the index.

Należy pamiętać, że w językach takich jak Java istnieje getPreviousSibling()funkcja, jednak w JS stała się ona właściwością - previousSibling.

Liv
źródło
2
Tak. Jednak w tekście zostawiłeś getPreviousSibling ().
Tim Down
7
to podejście wymaga takiej samej liczby iteracji, aby określić indeks podrzędny, więc nie widzę, jak byłoby to znacznie szybsze.
Michael
31
Wersja for (var i=0; (node=node.previousSibling); i++);
Scott Miles,
2
@sfarbota Javascript nie zna zakresu bloków, więc ibędzie dostępny.
A1rPun
3
@nepdev Wynika to z różnic między .previousSiblingi .previousElementSibling. Pierwsza trafia w węzły tekstowe, druga nie.
abluejelly
133

Polubiłem indexOfdo tego używać . Ponieważ indexOfjest włączony Array.prototypei parent.childrenjest NodeList, musisz go użyć. call();Jest trochę brzydki, ale ma jedną linijkę i wykorzystuje funkcje, które i tak powinien znać każdy programista javascript.

var child = document.getElementById('my_element');
var parent = child.parentNode;
// The equivalent of parent.children.indexOf(child)
var index = Array.prototype.indexOf.call(parent.children, child);
KhalilRavanna
źródło
7
var index = [] .indexOf.call (child.parentNode.children, child);
cuixiping
23
Fwiw, using []tworzy wystąpienie Array za każdym razem, gdy uruchamiasz ten kod, co jest mniej wydajne w przypadku pamięci i GC w porównaniu z użyciem Array.prototype.
Scott Miles
@ScottMiles Czy mogę prosić o wyjaśnienie, co powiedziałeś trochę więcej? Czy []pamięć nie jest czyszczona jako wartość śmieci?
mrReiha
14
Aby ocenić, [].indexOfsilnik musi utworzyć instancję tablicy, aby uzyskać dostęp do indexOfimplementacji w prototypie. Sama instancja pozostaje nieużywana (robi GC, to nie jest wyciek, to tylko marnowanie cykli). Array.prototype.indexOfuzyskuje dostęp do tej implementacji bezpośrednio bez przydzielania anonimowej instancji. Różnica będzie znikoma w prawie wszystkich okolicznościach, więc szczerze mówiąc, może nie warto się tym przejmować.
Scott Miles
3
Uważaj na błąd w IE! Obsługiwane przez Internet Explorer 6, 7 i 8, ale błędnie zawiera węzły komentarzy. Źródło „ developer.mozilla.org/en-US/docs/Web/API/ParentNode/…
Luckylooke
102

ES6:

Array.from(element.parentNode.children).indexOf(element)

Wyjaśnienie:

  • element.parentNode.children→ Zwraca braci z element, w tym ten element.

  • Array.from→ Rzutuje konstruktora childrenna Arrayobiekt

  • indexOf→ Możesz złożyć wniosek, indexOfponieważ masz teraz Arrayprzedmiot.

Abdennour TOUMI
źródło
2
Jak dotąd najbardziej eleganckie rozwiązanie :)
Amit Saxena,
1
Ale dostępne tylko w Chrome 45 i Firefox 32 i w ogóle niedostępne w Internet Explorerze.
Peter
Internet Explorer wciąż żyje? Wystarczy Jock .. Ok, więc trzeba PolyFill aby Array.fromprace nad Internet Explorer
Abdennour Toumi
9
Według MDN, wywołanie Array.from () creates a new Array instance from an array-like or iterable object. Utworzenie nowej instancji tablicy tylko w celu znalezienia indeksu może nie być wydajne dla pamięci lub GC, w zależności od częstotliwości operacji, w takim przypadku iteracja, jak wyjaśniono w zaakceptowanej odpowiedzi, byłaby większa ideał.
TheDarkIn1978
1
@ TheDarkIn1978 Zdaję sobie sprawę, że istnieje wymiana między elegancją kodu a wydajnością aplikacji 👍🏻
Abdennour TOUMI
38

ES - krótszy

[...element.parentNode.children].indexOf(element);

Operator spreadu jest do tego skrótem

philipp
źródło
To interesujący operator.
Wszyscy pracownicy są niezbędni
Jaka jest różnica między e.parentElement.childNodesi e.parentNode.children?
mesqueeb
4
childNodeszawiera również węzły tekstowe
philipp
Z Type 'NodeListOf<ChildNode>' must have a '[Symbol.iterator]()' method that returns an iterator.ts(2488)
maszynopisem
9

Dodawanie (z prefiksem dla bezpieczeństwa) element.getParentIndex ():

Element.prototype.PREFIXgetParentIndex = function() {
  return Array.prototype.indexOf.call(this.parentNode.children, this);
}
mikemaccana
źródło
1
Jest powód uciążliwości tworzenia stron internetowych: przedrostek-jumpy dev's. Dlaczego po prostu nie zrobić if (!Element.prototype.getParentIndex) Element.prototype.getParentIndex = function(){ /* code here */ }? W każdym razie, jeśli kiedykolwiek zostanie to zaimplementowane w standardzie w przyszłości, prawdopodobnie zostanie zaimplementowane jako getter element.parentIndex. Więc powiedziałbym, że najlepszym podejściem byłobyif(!Element.prototype.getParentIndex) Element.prototype.getParentIndex=Element.prototype.parentIndex?function() {return this.parentIndex}:function() {return Array.prototype.indexOf.call(this.parentNode.children, this)}
Jack Giffin
3
Ponieważ przyszłość getParentIndex()może mieć inny podpis niż Twoja implementacja.
mikemaccana
7

𝗣𝗿𝗼𝗼𝗳 𝗢𝗳 𝗔 𝗟𝗲𝘀𝘀 𝗘𝗳𝗳𝗶𝗰𝗶𝗲𝗻𝘁 𝗕𝗶𝗻𝗮𝗿𝘆 𝗦𝗲𝗮𝗿𝗰𝗵

Stawiam hipotezę, że biorąc pod uwagę element, w którym wszystkie jego elementy podrzędne są uporządkowane w dokumencie po kolei, najszybszym sposobem powinno być przeszukanie binarne, porównując pozycje elementów w dokumencie. Jednak, jak wprowadzono we wniosku, hipoteza zostaje odrzucona. Im więcej masz elementów, tym większy potencjał wydajności. Na przykład, gdybyś miał 256 elementów, to (optymalnie) musiałbyś sprawdzić tylko 16 z nich! Dla 65536 tylko 256! Wydajność rośnie do potęgi 2! Zobacz więcej liczb / statystyk. Odwiedź Wikipedię

(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);

Następnie sposób, w jaki go używasz, polega na uzyskaniu właściwości „parentIndex” dowolnego elementu. Na przykład sprawdź następujące demo.

(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>

Ograniczenia

  • Ta implementacja rozwiązania nie będzie działać w IE8 i starszych.

Wyszukiwanie binarne VS liniowe na 200 000 elementów (może spowodować awarię niektórych przeglądarek mobilnych, OSTROŻNIE!):

  • W tym teście zobaczymy, jak długo trwa wyszukiwanie liniowe, aby znaleźć środkowy element VS wyszukiwanie binarne. Dlaczego środkowy element? Ponieważ znajduje się w średniej lokalizacji wszystkich innych lokalizacji, więc najlepiej reprezentuje wszystkie możliwe lokalizacje.

Wyszukiwanie binarne

Wyszukiwanie liniowe wstecz (`lastIndexOf`)

Wyszukiwanie liniowe do przodu (`indexOf`)

PoprzedniElementSibling Counter Search

Zlicza liczbę PreviousElementSiblings, aby uzyskać parentIndex.

Brak wyszukiwania

Do analizy porównawczej, jaki byłby wynik testu, gdyby przeglądarka zoptymalizowała wyszukiwanie.

The Conculsion

Jednak po przejrzeniu wyników w Chrome wyniki są odwrotne od oczekiwanych. Głupsze wyszukiwanie liniowe do przodu było zaskakujące o 187 ms, 3850%, szybciej niż wyszukiwanie binarne. Najwyraźniej Chrome w jakiś magiczny sposób przechytrzył console.asserti zoptymalizował go lub (bardziej optymistycznie) Chrome wewnętrznie używa numerycznego systemu indeksowania dla DOM, a ten wewnętrzny system indeksowania jest ujawniany poprzez optymalizacje zastosowane Array.prototype.indexOfna HTMLCollectionobiekcie.

Jack Giffin
źródło
Wydajne, ale niepraktyczne.
applemonkey496
3

Użyj algorytmu wyszukiwania binarnego, aby poprawić wydajność, gdy węzeł ma dużo rodzeństwa.

function getChildrenIndex(ele){
    //IE use Element.sourceIndex
    if(ele.sourceIndex){
        var eles = ele.parentNode.children;
        var low = 0, high = eles.length-1, mid = 0;
        var esi = ele.sourceIndex, nsi;
        //use binary search algorithm
        while (low <= high) {
            mid = (low + high) >> 1;
            nsi = eles[mid].sourceIndex;
            if (nsi > esi) {
                high = mid - 1;
            } else if (nsi < esi) {
                low = mid + 1;
            } else {
                return mid;
            }
        }
    }
    //other browsers
    var i=0;
    while(ele = ele.previousElementSibling){
        i++;
    }
    return i;
}
cuixiping
źródło
Nie działa. Muszę zaznaczyć, że wersja IE i wersja „innej przeglądarki” obliczą inne wyniki. Technika "innych przeglądarek" działa zgodnie z oczekiwaniami, uzyskując n-tą pozycję pod węzłem nadrzędnym, jednak technika IE "Pobiera porządkową pozycję obiektu w kolejności źródłowej, gdy obiekt pojawia się w całej kolekcji dokumentu" ( msdn.microsoft .com / en-gb / library / ie / ms534635 (v = vs.85) .aspx ). Np. Otrzymałem 126 używając techniki "IE", a następnie 4 używając drugiej.
Christopher Bull
1

Miałem problem z węzłami tekstowymi i pokazywał zły indeks. Oto wersja, aby to naprawić.

function getChildNodeIndex(elem)
{   
    let position = 0;
    while ((elem = elem.previousSibling) != null)
    {
        if(elem.nodeType != Node.TEXT_NODE)
            position++;
    }

    return position;
}
John chce wiedzieć
źródło
0
Object.defineProperties(Element.prototype,{
group : {
    value: function (str, context) {
        // str is valid css selector like :not([attr_name]) or .class_name
        var t = "to_select_siblings___";
        var parent = context ? context : this.parentNode;
        parent.setAttribute(t, '');
        var rez = document.querySelectorAll("[" + t + "] " + (context ? '' : ">") + this.nodeName + (str || "")).toArray();
        parent.removeAttribute(t);            
        return rez;  
    }
},
siblings: {
    value: function (str, context) {
        var rez=this.group(str,context);
        rez.splice(rez.indexOf(this), 1);
        return rez; 
    }
},
nth: {  
    value: function(str,context){
       return this.group(str,context).indexOf(this);
    }
}
}

Dawny

/* html */
<ul id="the_ul">   <li></li> ....<li><li>....<li></li>   </ul>

 /*js*/
 the_ul.addEventListener("click",
    function(ev){
       var foo=ev.target;
       foo.setAttribute("active",true);
       foo.siblings().map(function(elm){elm.removeAttribute("active")});
       alert("a click on li" + foo.nth());
     });
bortunac
źródło
1
Czy możesz wyjaśnić, dlaczego przedłużasz okres Element.prototype? Funkcje wyglądają na przydatne, ale nie wiem, do czego służą (nawet jeśli nazwy są oczywiste).
A1rPun
@ rozszerz Element.prototype, powodem jest podobieństwo ... 4 ex elemen.children, element.parentNode itd ... więc w ten sam sposób adresujesz element.siblings ... metoda grupowa jest trochę skomplikowana, ponieważ chcę rozszerzyć trochę podobne podejście do elelemtów przez ten sam typ węzła i posiadające te same atrybuty, nawet bez tego samego przodka
bortunac
Wiem, czym jest rozszerzanie prototypów, ale lubię wiedzieć, jak jest używany twój kod. el.group.value()??. Mój pierwszy komentarz ma na celu poprawę jakości Twojej odpowiedzi.
A1rPun
metody grupy i rodzeństwa zwracają Array z założonymi elementami dom .. .... dzięki za komentarz i za komentarz
bortunac
Bardzo elegancki, ale też bardzo powolny.
Jack Giffin,
-1
<body>
    <section>
        <section onclick="childIndex(this)">child a</section>
        <section onclick="childIndex(this)">child b</section>
        <section onclick="childIndex(this)">child c</section>
    </section>
    <script>
        function childIndex(e){
            let i = 0;
            while (e.parentNode.children[i] != e) i++;
            alert('child index '+i);
        }
    </script>
</body>
ofir_aghai
źródło
Nie potrzebujesz tutaj jQuery.
Witalij Zdanevich