querySelectorAll 研讨

querySelectorAll 探讨

随着css selector engine在越来越多的javascript库中实现,进而影响了浏览器开发商,在现代(除了ie)的浏览器中都实现了css selector api : querySelector,querySelectorAll ,利用原生的 render engine ,速度得到了极大的提高,最终反馈到新的javascript库中,在进行选择器筛选时都要首先进行能力检测(feature test),适时地选择原生实现。


这里说明一下使用原生 querySelectorAll  会遇到的问题以及解决思路:


node.querySelectorAll (selector) ,根据规范定义:


The querySelectorAll() method on the NodeSelector interface must, when invoked, return a NodeList containing all of the matching Element nodes within the node’s subtrees, in document order. If there are no such nodes, the method must return an empty NodeList.


简单的说就是在节点的子树集合中查找符合css选择串的节点集合。


测试html结构:

 

<div id="test">
	<b id="wrong"></b>
	<div id="testInner">
		<b id="ok">
		</b>
	</div>
</div>
<div>
</div>
 

 

 

而下面例子就有点奇怪:

 

 

document.getElementById("test").querySelectorAll("div b"); // 2 : wrong , ok

document.getElementById("test").querySelectorAll("div"); // 1 : testInner
 

若只在节点的子树集合中则与第一句代码矛盾,若在包括节点的子树中查找则与第二句代码矛盾,一般的来说我们期望第二句代码的结果,当限定context为一个节点时,一个css 选择器期望是从context的子节点开始匹配而不是从context节点自身开始。


以及更加诡异的

 

<div><p id="foo"><span></span></p></div>

var foo = document.getElementById("foo");
// should return nothing
// will return the SPAN (booo!)
alert(foo.querySelectorAll('div span').length)
 

 

综合判断似乎浏览器的实现是:

 

element-rooted queries are handled by "finding all the elements that match the given selector -- rooted in the document -- then filtering by the ones that have the specified element as an ancestor."

 

先在文档中找出所有符合选择器的元素,然后再根据是否其祖先结点是context来进行筛选。


库的解决


对于最先实现css selector engine 的javascript库,选择的是更符合人们期望的:给定的css selector从context 的子节点开始匹配,而现在又要尽可能的利用原生的实现,对于这个问题为了兼容性考虑,要和以往的版本保持一致,也就决定了不能简单的调用 node.querySelectorAll



1.jquery


jquery 对于 context 为 document 即没有 context 的情况,调用 document.querySelectAll ,而对于context 为节点的情况,其实转而调用的是纯dom实现的选择器引擎:

 

/* 1.4.2

line : 3528
判断只能是document才能使用原生
context === document ,只使用 document.querySelectorAll
 if ( !seed && context.nodeType === 9 && !isXML(context) ) {
  try {
   return makeArray( context.querySelectorAll(query), extra );
  } catch(e){}
} 
 
 
line:2746 oldsizzel
判断必须是包含在context节点中
 contains(context, checkSet[i])) ) 
*/

$("#test").find("div b"); // 1 : ok
 

 

2.yui3

 

yui3 则是采用一种比较聪明的做法,利用 id 选择器强制限制context,例如


document.getElementById("test").querySelectorAll("div b") 则被强制转换为


document.getElementById("test").querySelectorAll("#test div b")

 

也即:document. querySelectorAll("#test div b")

 

这样强制下层选择器从子节点开始匹配:使得 context中的querySelectorAll 变得无关紧要

 

YUI({filter:"DEBUG"}).use("node",function(Y){
		
		/*line 2597
		 enforce for element scoping
            if (node.tagName) {
                node.id = node.id || Y.guid();
                prefix = '[id="' + node.id + '"] ';
            }
            
     然后 document.getElementById("test").querySelectorAll("#test div b");       
	*/
		Y.one("#test").all("div b"); // 1:ok
	});

 

3.Extjs


extjs 我认为采用的是一种比较落后的做法,虽然采用了创新的编译函数 做法,但其根本没有采用浏览器的原生实现,不过也自然免除了这种特殊情况的考虑:

 

Ext.get("test").select("div b") //1:ok

 

PS: john resig 很早就注意到了这个问题 ,并在 Secrets of the JavaScript Ninja   11章正式说明了这个情况,虽然解释比较奇怪:


When performing an element-rooted query (calling querySelector or
querySelectorAll relative to an element) the selector only checks to see if the final portion of the
selector is contained within the element.


 

书中提出了同yui3类似的方法,且不具有侵入性:

 

(function () {
    var count = 1;
    this.rootedQuerySelectorAll = function (elem, query) {
        var oldID = elem.id;
        //解决奇怪的浏览器api问题
        elem.id = "rooted" + (count++);
        //CSS Selector Engine
        try {
            return elem.querySelectorAll("#" + elem.id + " " + query);
        } catch(e) {
            throw e;
        } finally {
            //不具有侵入性  
            elem.id = oldID;
        }
    };
})();