function $$() { return Selector.findChildElements(document, $A(arguments)); }
这个类可以分成三个部分:第一个部分就是根据不同的浏览器,判断使用什么DOM操作方法。其中操作IE就是用普通的getElementBy* 系列方法;FF是document.evaluate;Opera和Safari是selectorsAPI。第二部分是对外提供的基本函数,像findElements,match等,Element对象里面的很多方法就是直接调用这个对象里面的方法。第三部分就是XPath等一些查询DOM的匹配标准,比如什么的字符串代表的意思是查找first-child,什么的字符串代表的是查询nth-child。
由于这个对象里面的方法很多,就不给出所有的源码了,其实我自己也仅仅看懂了一些方法的代码而已。这里根据浏览器的不同用一个简单的例子走一遍进行DOM选择的流程。在这个过程中给出需要的源代码,并加以说明。
具体的例子如下:
下面以FF为例进行说明,流程如下:
/*先找到$$方法,上面已经给出了,在这个方法里面将调用Selector的findChildElements方法,并且第一个参数为document,剩下参数为DOM查询字符串的数组*/ findChildElements: function(element, expressions) { //这里先调用split处理了一下字符串数组,判断是否合法,并且删除了空格 expressions = Selector.split(expressions.join(',')); //handlers里面包含了对DOM节点处理的一些方法,像concat,unique等 var results = [], h = Selector.handlers; //逐个处理查询表达式 for (var i = 0, l = expressions.length, selector; i 1) ? h.unique(results) : results; } //=================================================== //Selector.split方法: split: function(expression) { var expressions = []; expression.scan(/(([\w#:.~> ()\s-] |\*|\[.*?\]) )\s*(,|$)/, function(m) { //alert(m[1]); expressions.push(m[1].strip()); }); return expressions; } //=================================================== //Selector.handlers对象 handlers: { concat: function(a, b) { for (var i = 0, node; node = b[i]; i ) a.push(node); return a; }, //...省略一些方法 unique: function(nodes) { if (nodes.length == 0) return nodes; var results = [], n; for (var i = 0, l = nodes.length; i
//先看Selector的初始化部分 //可以看出初始化部分就是判断要用什么方法操作DOM,下面看一个这几个方法 var Selector = Class.create({ initialize: function(expression) { this.expression = expression.strip(); if (this.shouldUseSelectorsAPI()) { this.mode = 'selectorsAPI'; } else if (this.shouldUseXPath()) { this.mode = 'xpath'; this.compileXPathMatcher(); } else { this.mode = "normal"; this.compileMatcher(); } } //=================================================== //XPath,FF支持此种方法 shouldUseXPath: (function() { //下面检查浏览器是否有BUG,具体这个BUG是怎么回事,我在网上也没搜到。大概意思就是检查一下能否正确找到某个节点的个数 var IS_DESCENDANT_SELECTOR_BUGGY = (function(){ var isBuggy = false; if (document.evaluate && window.XPathResult) { var el = document.createElement('div'); el.innerHTML = '
//当判断要用XPath进行查询时,就开始调用compileXPathMatcher方法了 compileXPathMatcher: function() { //底下给出patterns,和xpath var e = this.expression, ps = Selector.patterns, x = Selector.xpath, le, m, len = ps.length, name; //判断是否缓存了查询字符串e if (Selector._cache[e]) { this.xpath = Selector._cache[e]; return; } // './/*'表示在当前节点下查询所有节点 不懂得可以去网上看一下XPath的表示方法 this.matcher = ['.//*']; //这里的le防止无限循环查找,那个正则表达式匹配除单个空格符之外的所有字符 while (e && le != e && (/\S/).test(e)) { le = e; //逐个查找pattern for (var i = 0; i\s*/ }, { name: 'adjacent', re: /^\s*\ \s*/ }, { name: 'descendant', re: /^\s/ }, { name: 'tagName', re: /^\s*(\*|[\w\-] )(\b|$)?/ }, { name: 'id', re: /^#([\w\-\*] )(\b|$)/ }, { name: 'className', re: /^\.([\w\-\*] )(\b|$)/ }, { name: 'pseudo', re: /^:((first|last|nth|nth-last|only)(-child|-of-type)|empty|checked|(en|d is)abled|not)(\((.*?)\))?(\b|$|(?=\s|[: ~>]))/ }, { name: 'attrPresence', re: /^\[((?:[\w-] :)?[\w-] )\]/ }, { name: 'attr', re: /\[((?:[\w-]*:)?[\w-] )\s*(?:([!^$*~|]?=)\s*((['"])([^]*?)|([^'"][^ \]]*?)))?\]/ } ], //============================================== /*当找到pattern之后,在用对应的name找到相应的查询字符串的xpath表示形式。比如上面的id,对应的就是id字符串,在compileXPathMatcher里面会判断xpath是字符串还是方法,是方法则会传进来相应的参数进行调用*/ xpath: { descendant: "//*", child: "/*", adjacent: "/following-sibling::*[1]", laterSibling: '/following-sibling::*', tagName: function(m) { if (m[1] == '*') return ''; return "[local-name()='" m[1].toLowerCase() "' or local-name()='" m[1].toUpperCase() "']"; }, className: "[contains(concat(' ', @class, ' '), ' #{1} ')]", id: "[@id='#{1}']", //...省略一些方法 //============================================== //下面进入Selector的findElements方法!!
findElements: function(root) { //判断root是否null,为null则设置成document root = root || document; var e = this.expression, results; //判断是用哪种模式操作DOM,在FF下是xpath switch (this.mode) { case 'selectorsAPI': if (root !== document) { var oldId = root.id, id = $(root).identify(); id = id.replace(/[\.:]/g, "\[content]"); e = "#" id " " e; } results = $A(root.querySelectorAll(e)).map(Element.extend); root.id = oldId; return results; case 'xpath': //下面看一下_getElementsByXPath方法 return document._getElementsByXPath(this.xpath, root); default: return this.matcher(root); } }, //=========================================== //这个方法其实就是把查找到的节点放到results里,并且返回,这里用到了document.evaluate,下面给出了这个方法详细解释的网址 if (Prototype.BrowserFeatures.XPath) { document._getElementsByXPath = function(expression, parentElement) { var results = []; var query = document.evaluate(expression, $(parentElement) || document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); for (var i = 0, length = query.snapshotLength; i
下面使用给出的例子连续起来解释一下:
首先$$里面调用findChildElements方法,expressions被设置为['#navbar a','#siderbar a']
下面调用:selector = new Selector(expressions[i].strip());新建一个Selector对象,调用initialize方法,也就是判断用什么DOM API,由于是FF,所以是this.shouldUseXPath(),然后调用compileXPathMatcher()
然后compileXPathMatcher()里面的 var e = this.expression,把e设置成'#navbar a',然后进入while循环,遍历patterns,检查查询字符串的匹配模式,这里根据pattern的正则表达式,找到{ name: 'id', re: /^#([\w\-\*] )(\b|$)/ },,所以name为id,当m = e.match(ps[i].re)匹配之后,m被设置成一个数组,其中m[0]就是整个匹配的字符串'#navbar',m[1]就是匹配的第一个分组字符串'navbar'
接下来判断Object.isFunction(x[name]),由于id对应的是字符串,所以执行new Template(x[name]).evaluate(m)),字符串:id: "[@id='#{1}']",中的#{1}被替换成m[1],即'navbar',最后把结果放到this.matcher中
然后通过把第一个匹配的字符串删除,e变成了' a',这里有一个空格!接下来继续进行匹配
这次匹配到的是:{ name: 'descendant', re: /^\s/ },然后找到xpath中对应的descendant项:descendant: "//*",然后把这个字符串放到this.matcher中,去掉空格e只剩下字符'a'了,继续匹配
这词匹配到的是:{ name: 'tagName', re: /^\s*(\*|[\w\-] )(\b|$)?/ },然后找到tagName对应的xpath项,
tagName: function(m) {
if (m[1] == '*') return '';
return "[local-name()='" m[1].toLowerCase()
"' or local-name()='" m[1].toUpperCase() "']";
}
是个方法,所以会调用x[name](m),而m[1]='a',返回下面的那串字符,然后在放到this.matcher里,这次e为空串,while的第一个条件不满足,退出循环,把this.matcher数组连接成一个xpath字符串: .//*[@id='navbar']//*[local-name()='a' or local-name()='A']
在初始化完Selector后,执行Selector的实例方法findElements,这里直接调用:document._getElementsByXPath(this.xpath, root);
在_getElementsByXPath方法里执行真正的DOM查询方法document.evaluate,最后返回结果
以上就是整个查询DOM在FF下的流程!
在IE下和Opera,safari下流程是一样的,只不过执行的具体方法略有不同,有兴趣可以自己研究研究,那些复杂的DOM选择操作就不举例子了。这里构造的流程是非常值得学习的,包括通过pattern模式匹配进行xpath的生成,把那些patterns,xpath等提出来。
可以看出来,写一个兼容所有浏览器的框架真是不容易!学习学习!