Странное поведение при использовании querySelector

#javascript

#javascript

Вопрос:

Насколько я понимаю, при использовании element.querySelector() запрос должен начинаться с определенного элемента.

Однако, когда я запускаю приведенный ниже код, он сохраняет выбранный первый DIV тег в определенном элементе.

 const rootDiv = document.getElementById('test');
console.log(rootDiv.querySelector('div').innerHTML);
console.log(rootDiv.querySelector('div > div').innerHTML);
console.log(rootDiv.querySelector('div > div > div').innerHTML);
console.log(rootDiv.querySelector('div > div > div > div').innerHTML);
console.log(rootDiv.querySelector('div > div > div > div > div').innerHTML);  
 <div>
  <div>
    <div id="test">
      <div>
        <div>
        This is content
        </div>
      </div>
    </div>
  </div>
</div>  

Как вы можете видеть, первые несколько результатов те же самые.
Это это ошибка? Или он будет запрашивать с самого начала документа?

Ответ №1:

Что querySelector делает, так это находит элемент где-то в документе, который соответствует переданному CSS-селектору, и затем проверяет, является ли найденный элемент потомком элемента, который вы вызвали querySelector . Он не начинается с элемента, для которого он был вызван, и не выполняет поиск вниз — скорее, он всегда начинается на уровне документа, ищет элементы, соответствующие селектору, и проверяет, что элемент также является потомком вызывающего элемента контекста. Это немного неинтуитивно.

Итак:

 someElement.querySelector(selectorStr)
  

похоже

 [...document.querySelectorAll(selectorStr)]
  .find(elm => someElement.contains(elm));
  

Возможным решением является использование :scope для указания того, что вы хотите, чтобы выделение начиналось с rootDiv , а не с document :

 const rootDiv = document.getElementById('test');
console.log(rootDiv.querySelector(':scope > div').innerHTML);
console.log(rootDiv.querySelector(':scope > div > div').innerHTML);
console.log(rootDiv.querySelector(':scope > div > div > div').innerHTML);  
 <div>
  <div>
    <div id="test">
      <div>
        <div>
        This is content
        </div>
      </div>
    </div>
  </div>
</div>  

:scope поддерживается во всех современных браузерах, кроме Edge.

Комментарии:

1. :scope поддерживается не всеми, но большинством основных браузеров. В любом случае, спасибо за объяснение, и это должно быть ответом. Спасибо.

2. :scope не работает с IE и Edge, я думаю, важно указать на это

3. Вот таблица совместимости браузера — мы должны сопротивляться еще несколько месяцев, так как Edge внедряет движок Chromium: P

4. Есть ли у вас какие-либо исходники для движка браузера, который действительно работает таким образом? — Запрос имел бы тот же результат, если бы движок запускался с первого div и проверял, соответствует ли он селектору запроса…

Ответ №2:

Принятый в настоящее время ответ каким-то образом дает правильное логическое объяснение того, что происходит, но фактически они неверны.

Element.querySelector запускает алгоритм сопоставления селектора с деревом, который идет от корневого элемента и проверяет, соответствуют ли его потомки селектору.

Сам селектор является абсолютным, он не имеет никаких знаний о документе и даже не требует, чтобы ваш элемент был добавлен к любому. И, кроме :scope атрибута, также не имеет значения, с каким корнем вы вызвали querySelector метод.

Если бы мы хотели переписать его сами, это было бы больше похоже

 const walker = document.createTreeWalker(element, {
  NodeFilter.SHOW_ELEMENT,
  { acceptNode: (node) => return node.matches(selector) amp;amp; NodeFilter.FILTER_ACCEPT }
});
return walker.nextNode();
  

 const rootDiv = document.getElementById('test');
console.log(querySelector(rootDiv, 'div>div').innerHTML);

function querySelector(element, selector) {
  const walker = document.createTreeWalker(element, 
    NodeFilter.SHOW_ELEMENT,
    {
      acceptNode: (node) => node.matches(selector) amp;amp; NodeFilter.FILTER_ACCEPT
    });
  return walker.nextNode();
};  
 <div>
  <div>
    <div id="test">
      <div>
        <div>
          This is content
        </div>
      </div>
    </div>
  </div>
</div>  

С большой разницей, что эта реализация не поддерживает специальный :scope селектор.

Вы можете подумать, что это одно и то же при переходе из документа или из корневого элемента, но это не только изменит производительность, но также позволит использовать этот метод, пока элемент не добавлен ни к какому документу.

 const div = document.createElement('div');
div.insertAdjacentHTML('beforeend', '<div id="test"><div class="bar"></div></div>')

console.log(div.querySelector('div>.bar')); // found
console.log(document.querySelector('div>.bar')); // null  

Точно так же сопоставление элементов в Shadow-DOM было бы невозможно, если бы у нас был только Document.querySelector.

Комментарии:

1. Хорошее объяснение — особенно немного об узлах, которые не являются частью документа. Это более четко представляет, что на самом деле делает движок браузера.

Ответ №3:

Селектор запросов div > div > div означает только:

Найдите div, у которого есть родительский элемент и дедушка, которые оба также являются div.

И если вы начнете с первого дочернего элемента test и проверите селектор, это правда. И это причина, по которой только ваш последний запрос выбирает самый внутренний div, поскольку у него есть первый предикат (найдите div с пра-пра-дедушкой-div), который не выполняется первым дочерним элементом test.

Селектор запросов будет проверять только потомков, но он будет оценивать выражение в области всего документа. Просто представьте селектор, подобный проверке свойств элемента — даже если вы просматриваете только дочерний элемент, он все равно является дочерним элементом своего родителя.