網頁制作poluoluo文章簡介:在網上發現一個JavaScript小型選擇器—mini,其介紹在這裡已經說得挺清楚了,就不再羅嗦了。簡單來說,mini選擇器只支持以下選擇語句.
在網上發現一個JavaScript小型選擇器—mini,其介紹在這裡已經說得挺清楚了,就不再羅嗦了。簡單來說,mini選擇器只支持以下選擇語句:
* `tag`
* `tag > .className`
* `tag > tag`
* `#id > tag.className`
* `.className tag`
* `tag, tag, #id`
* `tag#id.className`
* `.className`
* `span > * > b`
經過調查,以上選擇語句已經滿足了95%以上的需求。
mini選擇器實例代碼如下:
1.
var
pAnchors = mini(
'p > a'
);
// Returns an array.
2.
for
(
var
i = 0, l = pAnchors.length; i < l; ++i) {
3.
// Do stuff...
4.
}
下載源碼查看,發現源碼並不難,至少比jquery簡單得多,就想試著分析一下它的源碼,練練手,之前我是想分析jquery源碼的,但發現實在太難了,超出能力范圍了,還是先從簡單的代碼開始吧。
mini選擇器大體上,就是先把選擇語句最右邊的元素先選出來,再根據左邊的父元素層層過濾得到符合整個語句的元素。
例如”#a table .red”這個語句的選擇過程,就是先選出頁面上所有class=”red”的dom元素,再在選出來的元素中判斷其父元素是否為table,是則保存,不是則丟棄。這層篩選完後,把結果再進行一次篩選,判斷其父元素是否id=”a”,是則保留,不是則丟棄,最後就篩選出了符合”#a table .red”的所有dom元素。
其余細節的解析,我用注釋的方式加在代碼上了。我發現要把分析代碼的過程寫出來真是很難,代碼是看得懂,但就是表達不出來代碼的意思。我現在寫出來的那些注釋,似乎有點亂,估計別人也挺難看懂,不過當練兵吧,我在寫之前並沒有完全了解mini的原理,寫完後就清晰了,看跟寫還是有很大區別的,寫出來對自己挺有幫助。
有些地方其實我也不是知道得很清晰,可能會有錯誤存在。代碼裡我還有一些細節不理解,有疑問的地方我打上了**號,希望高手看到能告知吧~
在這裡可以看到,單獨選擇一個id占了所有選擇語句的一半以上,個人感覺mini沒有對id進行單獨優化,算是不足吧,並且就算只選擇一個id,mini(”#id”)返回的也是一個數組,很不方便,實用性不強。
源碼解析:
001.
//首先建立一個立刻執行的匿名函數,創建了一個閉包環境(function(){})(),所有代碼寫在裡面,相當於開辟一個私有領域,在裡面定義的變量不會影響到全局其他變量。
002.
//此匿名函數最後返回_find(),傳給全局變量mini,這樣就可以通過mini(selector, context)調用閉包裡的_find()進行查詢了。_find()是閉包裡唯一暴露給外部的函數,其他變量與函數都是私有的,在外部不可見,只能在內部調用。
003.
004.
var
mini = (
function
(){
005.
006.
var
snack = /(?:[\w\-\\.
#]+)+(?:\[\w+?=([\'"])?(?:\\\1|.)+?\1\])?|\*|>/ig,
007.
exprClassName = /^(?:[\w\-_]+)?\.([\w\-_]+)/,
008.
exprId = /^(?:[\w\-_]+)?
#([\w\-_]+)/,
009.
exprNodeName = /^([\w\*\-_]+)/,
010.
//輔助數組,是為了能像這樣方便寫代碼:(part.match(exprClassName) || na)[1]
011.
na = [
null
,
null
];
012.
013.
function
_find(selector, context) {
014.
015.
//沒有傳入context的話 就默認為document
016.
context = context || document;
017.
018.
//判斷是不是只是選擇id。這裡看起來,只是選擇id的話不能使用querySelectorAll?
019.
var
simple = /^[\w\-_
#]+$/.test(selector);
020.
021.
if
(!simple && context.querySelectorAll) {
022.
//如果DOM元素的querySelectorAll方法存在,立即用此方法查找DOM節點,並將結果轉換為Array返回。
023.
//querySelectorAll是w3c制定的查詢dom標准接口,目前四大個浏覽器(firefox3.1 opera10, IE 8, safari 3.1+)都已經支持這個方法,使用浏覽器原生支持的方法無疑可以很大地提高查詢效率。
024.
return
realArray(context.querySelectorAll(selector));
025.
}
026.
027.
//如果querySelectorAll不存在,就要開始折騰了。
028.
//首先如果查詢語句包含了逗號,就把用逗號分開的各段查詢分離,調用本身_find查找各分段的結果,顯然此時傳入_find的查詢字符串已經不包含逗號了
029.
//各分段查詢結果用concat連接起來,返回時使用下面定義的unique函數確保沒有重復DOM元素存在數組裡。
030.
if
(selector.indexOf(
','
) > -1) {
031.
var
split = selector.split(/,/g), ret = [], sIndex = 0, len = split.length;
032.
for
(; sIndex < len; ++sIndex) {
033.
ret = ret.concat( _find(split[sIndex], context) );
034.
}
035.
return
unique(ret);
036.
}
037.
038.
//如果不包含逗號,開始正式查詢dom元素
039.
//此句把查詢語句各個部分分離出來。snack正則表達式看不太懂,大致上就是把"#id div > p"變成數組["#s2", "b", ">", "p"],空格和">"作為分隔符
040.
var
parts = selector.match(snack),
041.
042.
//取出數組裡最後一個元素進行分析,由於mini庫支持的查詢方式有限,能確保在後面的片段一定是前面片段的子元素,例如"#a div",div就是#a的子元素 "#a > p" p是#a的直接子元素
043.
//先把匹配最後一個查詢片段的dom元素找出來,再進行父類過濾,就能找出滿足整句查詢語句的dom元素
044.
part = parts.pop(),
045.
046.
//如果此片段符合正則表達式exprId,那就是一個ID,例如"#header",如果是一個ID,則把ID名返回給變量id,否則返回null
047.
id = (part.match(exprId) || na)[1],
048.
049.
//此句使用a = b && c 的方式,如果b為真,則返回c值賦給a;如果b為假,則直接返回b值給a。(null undefined false 0 "" 等均為假)
050.
//在這個框架裡很多這樣的用法。如果已經確定此片段類型是ID,就不必執行正則表達式測試它是不是class類型或者node類型了。直接返回null。
051.
//否則就測試它是不是class類型或者node類型,並把名字返回給變量className和nodeName。
052.
className = !id && (part.match(exprClassName) || na)[1],
053.
nodeName = !id && (part.match(exprNodeName) || na)[1],
054.
055.
//collection是用來記錄查詢結果的
056.
collection;
057.
058.
//如果此片段是class類型,如".red",並且DOM的getElementsByClassName存在(目前Firefox3和Safari支持),直接用此方法查詢元素返回給collection
059.
if
(className && !nodeName && context.getElementsByClassName) {
060.
061.
collection = realArray(context.getElementsByClassName(className));
062.
063.
}
else
{
064.
//**不明白這裡為什麼先查詢nodeName再查詢className再查詢id,個人感覺把id提到前面來不是更能提高效率?
065.
//如果此片段是node類型,則通過getElementsByTagName(nodeName)返回相應的元素給collection。
066.
//如果此片段不是id和node,就會執行collection = realArray(context.getElementsByTagName('*')),返回頁面所有元素給collection,為篩選className做准備。
067.
collection = !id && realArray(context.getElementsByTagName(nodeName ||
'*'
));
068.
069.
//如果此片段是class類型,經過上面的步驟collection就儲存了頁面所有元素,把它傳進下面定義的filterByAttr函數,找出符合class="className"的元素
070.
if
(className) {
071.
collection = filterByAttr(collection,
'className'
, RegExp(
'(^|\\s)'
+ className +
'(\\s|$)'
));
072.
}
073.
074.
//此處查詢id,如果是id,就不需要考慮此片段的前面那些查詢片段,例如"div #a"只需要直接返回id為a的元素就行了。
075.
//直接通過getElementById把它變成數組返回,如果找不到元素則返回空數組
076.
if
(id) {
077.
var
byId = context.getElementById(id);
078.
return
byId?[byId]:[];
079.
}
080.
}
081.
082.
//parts[0]存在,則表示還有父片段需要過濾,如果parts[0]不存在,則表示查詢到此為止,返回查詢結果collection就行了
083.
//collection[0]存在表示此子片段查詢結果不為空。如果為空,不需要再進行查詢,直接返回這個空數組。
084.
//還有父片段需要過濾,查詢結果又不為空的話,執行filterParents過濾collection的元素,使之符合整個查詢語句,並返回結果。
085.
return
parts[0] && collection[0] ? filterParents(parts, collection) : collection;
086.
087.
}
088.
089.
function
realArray(c) {
090.
091.
/**
092.
* 把元素集合轉換成數組
093.
*/
094.
095.
try
{
096.
//數組的slice方法不傳參數的話就是一個快速克隆的方法
097.
//通過call讓傳進來的元素集合調用Array的slice方法,快速把它轉換成一個數組並返回。
098.
return
Array.prototype.slice.call(c);
099.
}
catch
(e) {
100.
//如果出錯,就用原始方法把元素一個個復制給一個新數組並返回。
101.
//**什麼時候會出錯?
102.
var
ret = [], i = 0, len = c.length;
103.
for
(; i < len; ++i) {
104.
ret[i] = c[i];
105.
}
106.
return
ret;
107.
}
108.
109.
}
110.
111.
function
filterParents(selectorParts, collection, direct) {
112.
113.
//繼續把最後一個查詢片段取出來,跟_find裡的part = parts.pop()一樣
114.
var
parentSelector = selectorParts.pop();
115.
116.
//記得分離選擇語句各個部分時,"#id div > p"會變成數組["#s2", "b", ">", "p"],">"符號也包含在內。
117.
//如果此時parentSelector是">",表示要查找的是直接父元素,繼續調用filterParents,並把表示是否只查找直接父元素的標志direct設為true。
118.
if
(parentSelector ===
'>'
) {
119.
return
filterParents(selectorParts, collection,
true
);
120.
}
121.
122.
//ret存儲查詢結果 跟_find()裡的collection一樣 r為ret的數組索引
123.
var
ret = [],
124.
r = -1,
125.
126.
//與_find()裡的定義完全一樣
127.
id = (parentSelector.match(exprId) || na)[1],
128.
className = !id && (parentSelector.match(exprClassName) || na)[1],
129.
nodeName = !id && (parentSelector.match(exprNodeName) || na)[1],
130.
131.
//collection的數組索引
132.
cIndex = -1,
133.
node, parent,
134.
matches;
135.
136.
//如果nodeName存在,把它轉成小寫字母以便比較
137.
nodeName = nodeName && nodeName.toLowerCase();
138.
139.
//遍歷collection每一個元素進行檢查
140.
while
( (node = collection[++cIndex]) ) {
141.
//parent指向此元素的父節點
142.
parent = node.parentNode;
143.
144.
do
{
145.
146.
//如果當前片段是node類型,nodeName是*的話無論如何都符合條件,否則應該讓collection裡元素的父元素的node名與之相等才符合條件
147.
matches = !nodeName || nodeName ===
'*'
|| nodeName === parent.nodeName.toLowerCase();
148.
//如果當前片段是id類型,就應該讓collection裡元素的父元素id與之相等才符合條件
149.
matches = matches && (!id || parent.id === id);
150.
//如果當前片段是class類型,就應該讓collection裡元素的父元素的className與之相等才符合條件
151.
//parent.className有可能前後包含有空格,所以用正則表達式匹配
152.
matches = matches && (!className || RegExp(
'(^|\\s)'
+ className +
'(\\s|$)'
).test(parent.className));
153.
154.
//如果direct=true 也就是說後面的符號是>,只需要查找直接父元素就行了,循環一次立刻break
155.
//另外如果找到了匹配元素,也跳出循環
156.
if
(direct || matches) {
break
; }
157.
158.
}
while
( (parent = parent.parentNode) );
159.
//如果一直篩選不到,則一直循環直到根節點 parent=false跳出循環,此時matches=false
160.
161.
//經過上面的檢查,如果matches=true則表示此collection元素符合條件,添加到結果數組裡。
162.
if
(matches) {
163.
ret[++r] = node;
164.
}
165.
}
166.
167.
//跟_find()一樣,此時collection變成了ret,如果還有父片段,繼續進行過濾,否則返回結果
168.
return
selectorParts[0] && ret[0] ? filterParents(selectorParts, ret) : ret;
169.
170.
}
171.
172.
var
unique = (
function
(){
173.
//+new Date()返回時間戳作為唯一標識符
174.
//為了保存變量uid和方法data,使用了一個閉包環境
175.
var
uid = +
new
Date();
176.
177.
var
data = (
function
(){
178.
//為了保存變量n,使用了一個閉包環境
179.
var
n = 1;
180.
181.
return
function
(elem) {
182.
183.
//如果elem是第一次進來檢驗,cacheIndex=elem[uid]=false,賦給elem[uid]一個值並返回true
184.
//下次再進來檢驗時elem[uid]有了值,cacheIndex!=flase 就返回false
185.
//**此處不明白nextCacheIndex的作用,隨便給elem[uid]一個值不就行了嗎
186.
var
cacheIndex = elem[uid],
187.
nextCacheIndex = n++;
188.
189.
if
(!cacheIndex) {
190.
elem[uid] = nextCacheIndex;
191.
return
true
;
192.
}
193.
194.
return
false
;
195.
196.
};
197.
198.
})();
199.
200.
return
function
(arr) {
201.
202.
var
length = arr.length,
203.
ret = [],
204.
r = -1,
205.
i = 0,
206.
item;
207.
208.
//遍歷每個元素傳進data()增加標志,判斷是否有重復元素,重復了就跳過,不重復就賦給ret數組
209.
for
(; i < length; ++i) {
210.
item = arr[i];
211.
if
(data(item)) {
212.
ret[++r] = item;
213.
}
214.
}
215.
216.
//下次調用unique()時必須使用不同的uid
217.
uid += 1;
218.
219.
//返回確保不會有重復元素的數組ret
220.
return
ret;
221.
222.
};
223.
224.
})();
225.
226.
function
filterByAttr(collection, attr, regex) {
227.
228.
/**
229.
* 通過屬性名篩選元素
230.
*/
231.
232.
var
i = -1, node, r = -1, ret = [];
233.
//遍歷collection裡每一個元素
234.
while
( (node = collection[++i]) ) {
235.
//整個框架調用filterByAttr的只有這一句:collection = filterByAttr(collection, 'className', RegExp('(^|\\s)' + className + '(\\s|$)'));
236.
//篩選元素的className,如果符合,加進數組ret,否則跳過
237.
if
(regex.test(node[attr])) {
238.
ret[++r] = node;
239.
}
240.
}
241.
//返回篩選結果
242.
return
ret;
243.
}
244.
245.
//返回_find,暴露給外部的唯一接口
246.
return
_find;
247.
248.
})();