it-roy-ru.com

Самый быстрый способ найти индекс дочернего узла в родительском

Я хочу найти индекс дочернего элемента div с идентификатором 'whereami'.

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

В настоящее время я использую эту функцию, чтобы найти индекс ребенка.

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

скрипка: http://jsfiddle.net/grantk/F7JpH/2/

Эта проблема
Когда есть тысячи узлов div, цикл while должен проходить через каждый div для их подсчета. Что может занять некоторое время.

Есть ли более быстрый способ справиться с этим?

* Обратите внимание, что идентификатор изменится на другой узел div, поэтому его нужно будет пересчитать.

31
gkiely

Из любопытства я запустил ваш код как для jQuery .index() , так и для моего кода ниже:

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

Перейти к результатам jsperf

Оказывается, что jQuery примерно на 50% медленнее, чем ваша реализация (на Chrome/Mac), и моя, вероятно, превысила его на 1%.

Правка

Не могу этого не допустить, поэтому я добавил еще два подхода:

Используя Array.indexOf

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

Улучшение моего более раннего экспериментального кода, как видно из ответа HBP , DOMNodeList обрабатывается как массив, и он использует Array.indexOf() для определения позиции в своем .parentNode.children, которые являются всеми элементами. Моей первой попыткой было использование .parentNode.childNodes, но это дает неверные результаты из-за текстовых узлов.

Используя previousElementSibling

Вдохновленный user1689607's answer , последние браузеры имеют другое свойство, кроме .previousSibling, называемого .previousElementSibling, которое выполняет оба исходных оператора в одном. IE <= 8 не имеет этого свойства, но .previousSibling уже действует как таковой, поэтому функция обнаружения будет работать.

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

Заключение 

Использование Array.indexOf() не поддерживается в IE <= 8 браузерах, и эмуляция просто не достаточно быстра; однако, это дает улучшение производительности на 20%.

Использование функции обнаружения и .previousElementSibling дает 7-кратное улучшение (в Chrome), я пока не тестировал его на IE8.

37
Ja͢ck

Выбрав ArrayindexOf, вы можете использовать:

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

[Проверено только в браузере Chrome]

5
HBP

Я добавил два теста к jsPerf test . Оба используют previousElementSibling, но второй включает код совместимости для IE8 и ниже.

Оба они работают очень хорошо в современных браузерах (которые в настоящее время используются большинством браузеров), но в старых браузерах будут иметь небольшой успех.


Вот первый, который не включает исправление совместимости. Он будет работать в IE9 и выше, а также практически во всех Firefox, Chrome и Safari.

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

Вот версия с исправлением совместимости.

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;
}

Поскольку он автоматически захватывает одноуровневых элементов, для nodeType тест не требуется, и цикл в целом короче. Это объясняет значительное увеличение производительности.


Я также добавил одну последнюю версию, которая зацикливает .children, и сравнивает node с каждой.

Это не так быстро, как версии previousElementSibling, но все же быстрее, чем остальные (по крайней мере, в 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;
}


Возвращаясь к версии previousElementSibling, приведу твик, который может немного повысить производительность.

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;
}

Я не проверял это в jsPerf, но разбить его на два разных цикла, основанных на наличии previouselementSibling, я бы только подумал.

Может быть, я добавлю это немного.

Я пошел дальше и добавил его в тест, связанный в верхней части этого ответа. Это немного помогает, так что я думаю, что это стоит того.

3
I Hate Lazy

Небольшое улучшение по сравнению с решением Джека, улучшение на 3%. Немного странно, действительно.

function findRow5(node)
{
    var i = 2;
    while (node = node.previousSibling)
        i += node.nodeType ^ 3;
    return i >> 1;
}

Поскольку в этом случае (и в большинстве случаев) существует только два возможных nodeTypes:

Node.ELEMENT_NODE == 1
Node.TEXT_NODE == 3

Так что xor 3 с nodeType, даст 2 и 0.

http://jsperf.com/sibling-index/4

2
xiaoyi

Вообще говоря, небольшая разница в производительности имеет незначительный эффект, если код не выполняется в цикле. Необходимость запускать код один раз, а не каждый раз, будет значительно быстрее.

Сделайте что-то подобное один раз:

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

Впоследствии найти индекс так же просто, как:

document.getElementById('findme').index

Похоже, что все, что вы делаете, может быть полезно, если вы будете иметь более четкие отношения между DOM и javascript. Подумайте об изучении Backbone.js, небольшой библиотеки MVC javascript, которая значительно облегчает управление веб-приложениями.

Правка: я удалил JQuery я использовал. Обычно я избегаю его использовать, но для SO это предпочтение, поэтому я предположил, что оно все равно будет использовано. Здесь вы можете увидеть очевидную разницу: http://jsperf.com/sibling-index/8

1
mowwwalker

Так как вы используете jQuery. Индекс должен сделать свое дело

jQuery('#whereami').index()

но как вы собираетесь использовать индекс позже?

1
fedmich

Попробуй это:

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

Я предполагаю, что, учитывая элемент, где все его дочерние элементы упорядочены в документе последовательно, самый быстрый способ должен состоять в том, чтобы выполнить бинарный поиск, сравнивая позиции документа в элементах. Однако, как указано в заключении, гипотеза отвергается. Чем больше элементов у вас есть, тем больше потенциал для производительности. Например, если у вас было 256 элементов, то (оптимально) вам нужно будет проверить только 16 из них! Для 65536 только 256! Производительность возрастает до 2! Посмотреть больше цифр/статистики. Посетите Википедия

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

Затем вы используете его, получая свойство parentIndex любого элемента. Например, посмотрите следующую демонстрацию.

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

Ограничения

  • Эта реализация решения не будет работать в IE8 и ниже.

Бинарный VS Линейный поиск На 200 тысячах элементов (может произойти сбой некоторых мобильных браузеров, ОСТЕРЕГАЙТЕСЬ!):

  • В этом тесте мы увидим, сколько времени занимает линейный поиск, чтобы найти средний элемент VS двоичного поиска. Почему средний элемент? Поскольку он находится в среднем местоположении всех других местоположений, поэтому он наилучшим образом представляет все возможные местоположения.

Бинарный поиск

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

Обратный (`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>

Вперед (`indexOf`) Линейный поиск

(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 Поиск

Подсчитывает количество PreviousElementSiblings, чтобы получить 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>

Нет поиска

Для оценки результатов теста, если бы браузер оптимизировал поиск.

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>

Сотрясение

Однако после просмотра результатов в Chrome результаты оказались противоположными ожидаемым. Линейный поиск в направлении «тупее вперед» оказался на удивление 187 мс, на 3850% быстрее, чем бинарный поиск. Очевидно, что Chrome каким-то волшебным образом перехитрил console.assert и оптимизировал его, или (более оптимистично) Chrome внутренне использует систему числового индексирования для DOM, и эта внутренняя система индексации предоставляется посредством оптимизаций, применяемых к Array.prototype.indexOf при использовании объекта HTMLCollection.

1
Jack Giffin