复制代码 代码如下:
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 //新建selector 
selector = new selector(expressions[i].strip()); 
//把查询到的节点连接到results里面 
h.concat(results, selector.findelements(element)); 
} 
//如果找到的节点数大于一,把重复节点过滤掉 
return (l > 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 if (typeof (n = nodes[i])._countedbyprototype == 'undefined') { 
n._countedbyprototype = prototype.emptyfunction; 
results.push(element.extend(n)); 
} 
return selector.handlers.unmark(results); 
},
//下面转向新建selector对象过程!!
复制代码 代码如下:
//先看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 = '
'; 
//这里的local-name()的意思就是去掉命名空间进行查找 
var xpath = .//*[local-name()='ul' or local-name()='ul'] + 
//*[local-name()='li' or local-name()='li']; 
//document.evaluate是核心的dom查询方法,具体的使用可以到网上搜 
var result = document.evaluate(xpath, el, null, 
xpathresult.ordered_node_snapshot_type, null);
isbuggy = (result.snapshotlength !== 2); 
el = null; 
} 
return isbuggy; 
})();
return function() { 
//返回的方法中判断是否支持此种dom操作。 
if (!prototype.browserfeatures.xpath) return false;
var e = this.expression; 
//这里可以看到safari不支持-of-type表达式和empty表达式的操作 
if (prototype.browser.webkit && 
(e.include(-of-type) || e.include(:empty))) 
return false;
if ((/(\[[\w-]*?:|:checked)/).test(e)) 
return false;
if (is_descendant_selector_buggy) return false;
return true; 
}
})(),
//===================================================
//sarafi和opera支持此种方法 
shoulduseselectorsapi: function() { 
if (!prototype.browserfeatures.selectorsapi) return false; 
//这里判断是否支持大小写敏感查找 
if (selector.case_insensitive_class_names) return false;
if (!selector._div) selector._div = new element('div'); 
//检查一下在空div里面进行查询是否会抛出异常 
try { 
selector._div.queryselector(this.expression); 
} catch(e) { 
return false; 
}
//===================================================
//selector.case_insensitive_class_names属性 
/*document.compatmode用来判断当前浏览器采用的渲染方式。 
当document.compatmode等于backcompat时,浏览器客户区宽度是document.body.clientwidth; 
当document.compatmode等于css1compat时,浏览器客户区宽度是document.documentelement.clientwidth。*/
if (prototype.browserfeatures.selectorsapi && 
document.compatmode === 'backcompat') { 
selector.case_insensitive_class_names = (function(){ 
var div = document.createelement('div'), 
span = document.createelement('span');
div.id = prototype_test_id; 
span.classname = 'test'; 
div.appendchild(span); 
var isignored = (div.queryselector('#prototype_test_id .test') !== null); 
div = span = null; 
return isignored; 
})(); 
}
return true; 
},
//===================================================
//如果这两个都不是就用document.getelement(s)by*系列方法进行处理,貌似ie8开始支持selectorapi了,其余版本ie就只能用普通的方法进行dom查询了
//下面转向ff支持的shouldusexpath方法!!!
复制代码 代码如下:
//当判断要用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//这里的name就是pattern里面对象的name属性 
name = ps[i].name; 
//这里查看表达式是否匹配这个pattern的正则表达式         
if (m = e.match(ps[i].re)) { 
/* 
注意这里,下面的xpath里面有的是方法,有的是字符串,所以这里需要判断一下,字符串的话,需要调用template的evaluate方法,替换里面的#{...}字符串;是方法的话,那就传入正确的参数调用方法 
*/ 
this.matcher.push(object.isfunction(x[name]) ? x[name](m) : 
new template(x[name]).evaluate(m)); 
//把匹配的部分去掉,继续下面的字符串匹配 
e = e.replace(m[0], '');
break; 
} 
}
} 
//把所有的匹配的xpath表达式连接起来,组成最终的xpath查询字符串 
this.xpath = this.matcher.join(''); 
//放到缓存中 
selector._cache[this.expression] = this.xpath; 
}, 
//==============================================
//这些patterns就是判断查询字符串到底是要查找什么,根据相应的整个表达式来判断,譬如字符串'#navbar'根据patterns匹配,那么就是id 
patterns: [ 
{ name: 'latersibling', re: /^\s*~\s*/ }, 
{ name: 'child', re: /^\s*>\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*((['])([^\4]*?)\4|([^'][^ 
\]]*?)))?\]/ } 
],
//==============================================
/*当找到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, \\$0); 
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 results.push(element.extend(query.snapshotitem(i))); 
return results; 
}; 
}
/* 
下面这个网址是document.evaluate的方法解释:https://developer.mozilla.org/cn/dom/document.evaluate 
*/
下面使用给出的例子连续起来解释一下:
首先$$里面调用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等提出来。
可以看出来,写一个兼容所有浏览器的框架真是不容易!学习学习!
   
 
   