这几天在写一个简单的只针对单Dom节点的jquery,只考虑IE8+。jq里有个is方法,非常好用,他的用法是:
1 | $('#div1').is('#content .sidebar'); |
这样就可以判断$(‘#div1’)是否是位于#content下面,且具有.sidebar的className了。我也要实现一个。
本来我昨天已经实现了,但今天在搜索mouseleave相关问题时,居然意外的发现了更好的解决方式。就是Element.matches(),他用来判断element是否匹配给定的选择器。
他的使用方式简单得和jq.is一样:
1 | node.matches('#content .sidebar'); |
当然,这么好的方法,IE8是不支持的,有的旧的现代浏览器也不支持,但MDN直接给出了polyfill:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | if (!Element.prototype.matches) { Element.prototype.matches = Element.prototype.matchesSelector || Element.prototype.mozMatchesSelector || Element.prototype.msMatchesSelector || Element.prototype.oMatchesSelector || Element.prototype.webkitMatchesSelector || function(s) { var matches = (this.document || this.ownerDocument).querySelectorAll(s), i = matches.length; while (--i >= 0 && matches.item(i) !== this) {} return i > -1; }; } |
其中,老式的现代浏览器及IE9,使用带私有前缀的matchesSelector方法即可(包括IE9),连matchesSelector也没有的(比如IE8),就用querySelectorAll模拟。
问题几乎就解决了,但对IE8有个问题。
querySelectorAll有个特点,就是选择器越简单,得到的结果就越多。比如:querySelectorAll(‘div’),这样会得到页面上所有DIV,可能有几百个。
而我们想要的matches方法,当然越快越好。假如我们这样写:
1 | node.matches('div'); |
本意只想判断一下此node的tagName==div,但在IE8下,会先得到所有的div,再遍历进行判断,消耗非常大。本来IE就很卡了,这样一来更是雪上加霜。
这样的matches函数,就进入了一个矛盾之地:越简单的判断,反而性能越低。
改进办法
querySelectorAll是选一群,querySelector则是选一个,后者当然速度更快,但怎么使querySelector能选到我期望中的那一个呢?我的办法是加ID。比如我要判断:
1 | node.matches('#content .sidebar'); |
此时,如果node有id,为#div1,则我会把选择器改为’#content #div1.sidebar’;如果node没有id,则我会生成一个临时ID,如#tempid_1234,然后再把选择器改为’#content #tempid_1234.sidebar’,获取到节点后,再恢复ID为空。
将改造后的选择器字符串,配合querySelector,就能匹配到我想要的、惟一的那个节点了。
当然,如果本来就不匹配,则改造后的选择器,还是不会选择到那个节点,不然就影响正确性了。
获取到节点后,直接node===获取到的节点,就知道匹配与否了。
相关代码在dd.js里,摘录:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | is: function(str) { var node = this.node; if (typeof str === 'function') { return str(node); } else { var temp = tempId(node); var lastBlank = str.lastIndexOf(' '); // 如果是多级选择器,比如#id .class tagname if (lastBlank > -1) { // 如果最后一截选择器不是以#开头的 lastBlank += 1; if (str.charAt(lastBlank) !== '#') { var last = str.slice(lastBlank); var idstr = '#' + node.id; // 如果是以字母开头(即tagName),则把id加到后面;以其他开头(.,[]),则把id加到前面 if (/[a-z]/i.test(last[0])) { last += idstr; } else { last = idstr + last; } str = str.slice(0, lastBlank) + last; } var $node = $(str); temp.restore(); // console.log(str, $node); return $node === node; } return is(node, str); } } |
其中的$就是querySelector。这里面还涉及到对选择器字符串的判断(判断最后一级是否已带ID选择器),而tempId只是一个判断并加上临时ID的方法。
此时,要判断这个:
1 | node.matches('div'); |
则会变成:
1 | node.matches('div#tempid_123113'); |
将会结合querySelector非常快速的产生结果(虽然还是比IE9原生慢10几倍)。
继续调优
现在问题已经解决得差不多了,只是我对简单判断还是有意见,越难的条件越难判断是正常的,条件简单,那我们也应该使用更直接的判断。所以我写了个is方法,用来解决“只有1级选择器”的情况。如:
1 | node.matches('div#id.clss1.clss2[data-holder="ggg"]'); //虽然这么长,但我就是只有一级 |
当然,这只针对IE8了。有兴趣可以去dd.js的github地址查看源码。
经测试,使用querySelector模拟的方式,在chrome里要比原生慢5倍左右;IE9模拟比IE9差不多;IE8模拟比IE9原生慢5倍左右。
而对一级选择器使用单独的函数,在IE8下与使用querySelector效率差不多,看来这个独立的is方法还有优化空间。我再想想
参考:https://developer.mozilla.org/en-US/docs/Web/API/Element/matches