Javascript:Element.matches()与jQuery.is的实现

这几天在写一个简单的只针对单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

发表评论

电子邮件地址不会被公开。