使用querySelector时出现奇怪的行为

35

据我理解,使用 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>

正如你所看到的,前几个结果是相同的。这是一个bug吗?还是它会从文档开头查询?

3个回答

29
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。


:scope并非所有主流浏览器都支持。无论如何,感谢解释,这应该是答案。谢谢。 - Joey Chong
:scope 在 IE 和 Edge 中不起作用,我认为指出这一点很重要。 - Christian Vincenzo Traina
这里是浏览器兼容性表格 - 我们只需要再坚持几个月,因为Edge正在实现Chromium引擎 :P - Christian Vincenzo Traina
你有关于浏览器引擎实际上是这样工作的任何来源吗?- 如果引擎从第一个 div 开始并检查它是否与查询选择器匹配,那么查询将具有相同的结果... - Falco

7

目前被接受的回答提供了一个有效的逻辑解释,阐述了发生了什么,但它们在事实上是错误的。

Element.querySelector触发了从根元素匹配选择器的算法,它从根元素开始检查其后代是否与选择器匹配

选择器本身是绝对的,不需要了解文档信息,甚至不需要将您的元素附加到任何文档中。除了:scope属性外,它也不关心您调用querySelector方法时使用的哪个

如果我们自己想重新编写它,可能更像:

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中的元素将是不可能的。

很好的解释 - 特别是关于节点不属于文档的那部分。这更清楚地说明了浏览器引擎实际上做了什么。 - Falco

1

查询选择器div > div > div的意思是:

查找一个具有父级和祖父级都是div的div。

如果你从test的第一个子元素开始检查选择器,它是正确的。这就是为什么只有你最后一个查询选择了最内层的div,因为它具有第一个谓词(查找一个具有曾祖父div的div),而test的第一个子元素不符合此条件。

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接