第6章 选择器引擎
jQuery凭借选择器风靡全球,从而使各大框架类库争先开发自己的选择器,一时间选择器成为框架的标配。
其实,早期jQuery选择器与我们现在看到的大不一样。它最初是使用混杂xpath语法的selector,第二代转为纯CSS带自定义伪类(比如从xpath借鉴过来位置伪类)的Sizzle。但Sizzle也一直在变,因为它的关系选择器一直存在问题,因此不断重构,在jQuery1.9时终于搞定,并最终决定全面支持CSS3的结构伪类。
有据可查的早期三大选择器引擎是2003年Simon Willison的getElementsBySelector,然后是2004年Dean Edwards的cssQuery,jQuery是2005年发布,据John Resig在《JavaScript精粹》说,他本来只想写个选择器引擎,但 cssQuery“光芒太盛”,无法与之争锋,匆忙间作为一个较为完整的dom类库面世。
2005年,Ben Nolan的Behaviour.js,内置了早以闻名于世的getElementsBySelector,是第一个集成事件处理、CSS风格的选择器引擎与onload处理的类库。此外日后霸主Prototype.js也在2005年诞生。但它勉强称得上是,选择器$与getElementsByClassName在1.2出现,事件处理在1.3,因此Behaviour还能风光一时。你们可以到这里下载Behaviour.js:
http://www.ccs.neu.edu/home/dherman/javascript/behavior/
本章介绍如何从头到尾制造一个选择器引擎,在此我们先看看前人的努力吧。
6.1 浏览器内置的寻找元素的方法
请不要追问2005年之前开发人员是怎么在这种缺东缺西的环境下干活的,那时浏览器大战打得正酣,程序员们发明了navigator.userAgent检测进行“自保”!网景战败,因此有关它的记录不多。但IE确确切切留下许多资料,比如取得元素,我们直接可以根据元素的ID就取得元素自身,不通过任何API,自动映射成全局变量。在不关注全局污染时,这是很酷的特性。又如取得所有元素,直接document.all。取得某一种标签类型的元素,只需做一下分类,如P标签,document.all.tags("p"),时至今天,IE4这个古老API还能在IE10标准模式下正常运作!
有资料可查的是getElementById、getElementsByTagName是IE5引入的,那是1999年的事,与微软另一个辉煌的产品Windows98,捆绑在一起。因此,那时的程序的代码都倾向于为IE做兼容。我在网上找到一个让IE4支持getElementById的代码,刻着时代的“烙印”。
var ie4=document.all && !document.getElementById; if(ie4) { document.getElementById = new Function('var expr = /^\\w[\\w\\d]*$/,'+ 'elname=arguments[0]; if(!expr.test(elname)) { return null; } '+ 'else if(eval("document.all."+elname)) { return '+ 'eval("document.all."+elname); } else return null;') }
此外还有getElementsByTagName的实现。
function getElementsByTagName(str) { if (str == "*") { return document.all } else { return document.all.tags(str) } }
但人们很快就发现问题了,IE的getElementById是不区分表单元素ID与Name,因此如果有一个表单元素只定义name并与我们的目标元素ID同名,且我们的目标元素在它的后面,那么就会选错元素。这个问题一直延续到IE7。
IE 的 getElementsByTagName 也有问题。当参数为*号通配符时,它会混入注释节点,并且无法选取Object下的元素。
下面是解决方法。
//J. Max Wilson if (/msie/i.test (navigator.userAgent)) {//only override IE document.nativeGetElementById = document.getElementById; document.getElementById = function(id){ var elem = document.nativeGetElementById(id); if(elem){//IE5 if(elem.id == id){ return elem; }else{//IE4 for(var i=1;i<document.all[id].length;i++){ if(document.all[id][i].id == id){ return document.all[id][i]; } } } } return null; }; } //Dean Edwards function getElementsByTagName(node, tagName) { var elements = [], i = 0, anyTag = tagName === "*", next = node.firstChild; while ((node = next)) { if (anyTag ? node.nodeType === 1 : node.nodeName === tagName) elements[i++] = node; next = node.firstChild || node.nextSibling; while (!next && (node = node.parentNode)) next = node.nextSibling; } return elements; };
此外W3C还提供了一个getElementsByName的方法,这个IE也有问题,它只能选取表单元素,由于我们后面用不到它,先行略去。
这是Prototype.js到来之前,所有可用的原生选择器。因此Simon Willison搞出getElementsBy Selector,让世人眼前一亮。
之后的情况大家应该知道了,出现N个版本的getElementsBySelector。不过大多数是在Simon Willison的基础上改进的,甚至当时还讨论将它标准化!
http://lists.whatwg.org/pipermail/whatwg-whatwg.org/2005-September/subject.html#4782
虽然这个打算最后搁浅了,但Simon Willison的getElementsBySelector代表的是历史的前进方向。jQuery则有点偏向了。Prototype.js则在Ajax热炒浪潮中扶摇直上,1.4在document添加日后成为标准的getElementsByClassName与失败了的getElementsBySelector,此外还有比jQuery正更统些的$$。不过,jQuery最终还是胜利了,Sizzle的设计很特别,各种优化别出心裁。
浏览器没有闲着,Netscape借Firefox还魂,挑起第二次浏览器战争,其间往HTML引入XML的xpath,其API为document.evaluate。但xpath又分为level1、level2、level3,各浏览器在不同版本的支持又不一致,加之语法比较复杂,因此普及不开,更甭论存在什么 BUG。同一时间还有getElementsByClassName,这个也通常只见于选择器引擎的内部应用,它也存在 BUG,分别位于Safari与Opera(下面章节会介绍)。
微软为了保住占有率,在IE8 上加入querySelector与querySeletorAll,相当于getElementsBy Selector的升级版,它还支持前所未有的结构伪类、状态伪类、语言伪类与取反伪类。这时Chrome参战,激发标准浏览器的升级热情,IE8新加的选择器大家都支持了,还支持得更标准。此时还出现了一种类此选择器的匹配器——matchesSelector ,它对我们编写选择器引擎非常有帮助,由于是在版本号竞赛时诞生的,谁也不能担保自己的实现被W3C采纳,因此都带有私有前缀。现在CSS方面有关selector 4的规范还在起草中,querySeletorAll也暂只支持到selector 3部分,但其间的兼容性问题已经很杂乱了。