複製代碼 代碼如下: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選擇的流程。在這個過程中給出需要的原始碼,並加以說明。
具體的例子如下: 複製代碼 代碼如下:<div id="parent2">
<div id="navbar">
<a id="n1"></a>
<a></a>
</div>
<div id="sidebar">
<a id="s1"></a>
<a></a>
</div>
</div>
<script type="text/javascript"><!--
$$('#navbar a', '#sidebar a')
// --></script>
下面以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 < l; 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 < l; 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 = '<ul><li></li></ul><div><ul><li></li></ul></div>';
//這裡的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防止無限迴圈尋找,那個Regex匹配除單個空格符之外的所有字元
while (e && le != e && (/\S/).test(e)) {
le = e;
//逐個尋找pattern
for (var i = 0; i<len; i++) {
//這裡的name就是pattern裡面對象的name屬性
name = ps[i].name;
//這裡查看錶達式是否匹配這個pattern的Regex
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 < length; 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的Regex,找到{ 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等提出來。
可以看出來,寫一個相容所有瀏覽器的架構真是不容易!學習學習!