網頁制作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.})();