使用querySelector时的怪异行为

时间:2019-04-08 07:27:11

标签: 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>

如您所见,前几个结果是相同的。 这是一个错误吗?还是会从文档开始处查询?

3 个答案:

答案 0 :(得分:25)

querySelector的作用是在文档中的某个地方找到一个与传递的CSS选择器匹配的元素,然后 then 检查所找到的元素是否是后代您调用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>

除Edge之外,所有现代浏览器均支持

:scope

答案 1 :(得分:7)

当前接受的答案以某种方式为发生的事情提供了有效的逻辑解释,但实际上是错误的。

Element.querySelector触发match a selector against tree算法,该算法从根元素开始 ,并检查其后代是否与选择器匹配

选择器本身是绝对的,它对文档没有任何了解,甚至不需要将Element附加到任何内容。除了:scope属性外,它也与您调用querySelector方法的 root 无关。

如果我们想自己重写它,那就更像

const walker = document.createTreeWalker(element, {
  NodeFilter.SHOW_ELEMENT,
  { acceptNode: (node) => return node.matches(selector) && 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) && 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

以同样的方式,如果我们只有Document.querySelector,则无法匹配Shadow-DOM中的元素。

答案 2 :(得分:1)

查询选择器div > div > div仅表示:

找到一个既有父级又有祖父母的div,它们也是一个div。

如果您从 test 的第一个孩子开始并检查选择器,则它为true。这就是为什么只有您的最后一个查询才选择最里面的div的原因,因为它具有第一个谓词(用一个great-grand-grandparent-div查找div),而 test的第一个子对象无法满足

查询选择器将仅测试后代,但它将评估整个文档范围内的表达式。试想一下,选择器就像检查元素的属性一样,即使您仅查看子元素,它仍然是其父元素的子元素。