dojo/query模塊是dojo為開發者提供的dom查詢接口。該模塊的輸出對象是一個使用css選擇符來查詢dom元素並返回NodeList對象的函數。同時,dojo/query模塊也是一個插件,開發者可以使用自定義的查詢引擎,query模塊會負責將引擎的查詢結果包裝成dojo自己的NodeList對象。
require(["dojo/query!sizzle"], function(query){ query("div")...
要理解這個模塊就要搞清楚兩個問題:
- 如何查詢,查詢的原理?
- 查詢結果是什么,如何處理查詢結果?
這兩個問題涉及到本文的兩個主題:選擇器引擎和NodeList。
選擇器引擎
前端的工作必然涉及到與DOM節點打交道,我們經常需要對一個DOM節點進行一系列的操作。但我們如何找到這個DOM節點呢,為此我們需要一種語言來告訴瀏覽器我們想要就是這個語言描述的dom節點,這種語言就是CSS選擇器。比如我們想瀏覽器描述一個dom節點:div > p + .bodhi input[type="checkbox"],它的意思是在div元素下的直接子元素p的下一個class特性中含有bodhi的兄弟節點下的type屬性是checkbox的input元素。
選擇符種類
- 元素選擇符:通配符*、類型選擇符E、類選擇符E.class、ID選擇符E#id
- 關系選擇符:包含(E F)、子選擇符(E>F)、相鄰選擇符(E+F)、兄弟選擇符(E~F)
- 屬性選擇符: E[att]、E[att="val"]、E[att~="val"]、E[att^="val"]、E[att$="val"]、E[att*="val"]
- 偽類選擇符
- 偽對象選擇符:E:first-letter、E:first-line、E:before、E:after、E::placehoser、E::selection
通過選擇器來查詢DOM節點,最簡單的方式是依靠瀏覽器提供的幾個原生接口:getElementById、getElementsByTagName、getElementsByName、getElementsByClassName、querySelector、querySelectorAll。但因為低版本瀏覽器不完全支持這些接口,而我們實際工作中有需要這些某些高級接口,所以才會有各種各樣的選擇器引擎。所以選擇器引擎就是幫我們查詢DOM節點的代碼類庫。
選擇器引擎很簡單,但是一個高校的選擇器引擎會涉及到詞法分析和預編譯。不懂編譯原理的我表示心有余而力不足。
但需要知道的一點是:解析css選擇器的時候,都是按照從右到左的順序來的,目的就是為了提高效率。比如“div p span.bodhi”;如果按照正向查詢,我們首先要找到div元素集合,從集合中拿出一個元素,再找其中的p集合,p集合中拿出一個元素找class屬性是bodhi的span元素,如果沒找到重新回到開頭的div元素,繼續查找。這樣的效率是極低的。相反,如果按照逆向查詢,我們首先找出class為bodhi的span元素集合,在一直向上回溯看看祖先元素中有沒有選擇符內的元素即可,孩子找父親很容易,但父親找孩子是困難的。
選擇器引擎為了優化效率,每一個選擇器都可以被分割為好多部分,每一部分都會涉及到標簽名(tag)、特性(attr)、css類(class)、偽節點(persudo)等,分割的方法與選擇器引擎有關。比如選擇器 div > p + .bodhi input[type="checkbox"]如果按照空格來分割,那它會被分割成以下幾部分:- div
- >
- p
- +
- .bodhi
- input[type="checkbox"]
對於每一部分選擇器引擎都會使用一種數據結構來表達這些選擇符,如dojo中acme使用的結構:
{ query: null, // the full text of the part's rule pseudos: [], // CSS supports multiple pseud-class matches in a single rule attrs: [], // CSS supports multi-attribute match, so we need an array classes: [], // class matches may be additive, e.g.: .thinger.blah.howdy tag: null, // only one tag... oper: null, // ...or operator per component. Note that these wind up being exclusive. id: null, // the id component of a rule getTag: function(){ return caseSensitive ? this.otag : this.tag; } }
從這里可以看到有專門的結構來管理不同的類型的選擇符。分割出來的每一部分在acme中都會生成一個part,part中有tag、偽元素、屬性、元素關系等。。;所有的part都被放到queryParts數組中。然后從右到左每次便利一個part,低版本瀏覽器雖然不支持高級接口,但是一些低級接口還是支持的,比如:getElementsBy*;對於一個part,先匹配tag,然后判斷class、attr、id等。這是一種解決方案,但這種方案有很嚴重的效率問題。(后面這句是猜想)試想一下:我們可不可以把一個part中有效的幾項的判斷函數來組裝成一個函數,對於一個part只執行一次即可。沒錯,acme就是這樣來處理的(這里涉及到預編譯問題,看不明白的自動忽略即可。。。)
1 define(["../has", "require"], 2 function(has, require){ 3 4 "use strict"; 5 var testDiv = document.createElement("div"); 6 has.add("dom-qsa2.1", !!testDiv.querySelectorAll); 7 has.add("dom-qsa3", function(){ 8 // test to see if we have a reasonable native selector engine available 9 try{ 10 testDiv.innerHTML = "<p class='TEST'></p>"; // test kind of from sizzle 11 // Safari can't handle uppercase or unicode characters when 12 // in quirks mode, IE8 can't handle pseudos like :empty 13 return testDiv.querySelectorAll(".TEST:empty").length == 1; 14 }catch(e){} 15 }); 16 var fullEngine; 17 var acme = "./acme", lite = "./lite"; 18 return { 19 // summary: 20 // This module handles loading the appropriate selector engine for the given browser 21 22 load: function(id, parentRequire, loaded, config){ 23 var req = require; 24 // here we implement the default logic for choosing a selector engine 25 id = id == "default" ? has("config-selectorEngine") || "css3" : id; 26 id = id == "css2" || id == "lite" ? lite : 27 id == "css2.1" ? has("dom-qsa2.1") ? lite : acme : 28 id == "css3" ? has("dom-qsa3") ? lite : acme : 29 id == "acme" ? acme : (req = parentRequire) && id; 30 if(id.charAt(id.length-1) == '?'){ 31 id = id.substring(0,id.length - 1); 32 var optionalLoad = true; 33 } 34 // the query engine is optional, only load it if a native one is not available or existing one has not been loaded 35 if(optionalLoad && (has("dom-compliant-qsa") || fullEngine)){ 36 return loaded(fullEngine); 37 } 38 // load the referenced selector engine 39 req([id], function(engine){ 40 if(id != "./lite"){ 41 fullEngine = engine; 42 } 43 loaded(engine); 44 }); 45 } 46 }; 47 });
選擇器引擎的代碼晦澀難懂,我們只需要關心最終暴露出來的接口的用法即可。
acme:
query = function(/*String*/ query, /*String|DOMNode?*/ root) ............ query.filter = function(/*Node[]*/ nodeList, /*String*/ filter, /*String|DOMNode?*/ root) ............ return query;
lite:
liteEngine = function(selector, root) ............... liteEngine.match = function(node, selector, root) .............. return liteEngine
NodeList
objects are collections of nodes such as those returned by Node.childNodes and the document.querySelectorAll method.
NodeList is a static collection, meaning any subsequent change in the DOM does not affect the content of the collection. document.querySelectorAll returns a static NodeList.
- dojo中的NodeList就是擴展了能力的Array實例。所以需要一個函數將原生array包裝起來
- NodeList的任何方法返回的還是NodeList實例。就像Array的slice、splice還是返回一個array一樣
has("array-extensible")的作用是判斷數組是否可以被繼承,如果原生的數組是可被繼承的,那就將NodeList的原型指向一個數組實例,否則指向普通對象。
var nl = NodeList, nlp = nl.prototype = has("array-extensible") ? [] : {};// extend an array if it is extensible
下面這句話需要扎實的基本功,如果理解這句話,整個脈絡就會變得清晰起來。
var NodeList = function(array){ var isNew = this instanceof nl && has("array-extensible"); // 是不是通過new運算符模式調用 。。。。。。。。。。 };
new的作用等於如下函數:
Function.prototype.new = function(){ // this指向的new運算符所作用的構造函數 var that = Object.create(this.prototype); var other = this.apply(that, arguments); return (other && typeof other === 'object') || that; }
放到NodeList身上是這樣的:
var nl = new NodeList(array); //等於一下操作 var that = Object.create(NodeList.prototype); //這時候NodeList中的this關鍵字指向that,that是NodeList的實例 var other = NodeList.apply(that, array); nl = (other && typeof other === 'object') || that;
isNew為true,保證了NodeList實例是一個經過擴展的array對象。
NodeList函數的源碼:
var NodeList = function(array){ var isNew = this instanceof nl && has("array-extensible"); // 是不是通過new運算符模式調用 if(typeof array == "number"){ array = Array(array); // 如果array是數字,就創建一個array數量的數組 } //如果array是一個數組或類數組對象,nodeArray等於array否者是arguments var nodeArray = (array && "length" in array) ? array : arguments; //如果this是nl的實例或者nodeArray是類數組對象,則進入if語句 if(isNew || !nodeArray.sort){ // make sure it's a real array before we pass it on to be wrapped //if語句的行為保證了經過該函數包裝后的對象的是一個真正的數組對象。 var target = isNew ? this : [], l = target.length = nodeArray.length; for(var i = 0; i < l; i++){ target[i] = nodeArray[i]; } if(isNew){//這時候便不再需要擴展原生array了 return target; } nodeArray = target; } // called without new operator, use a real array and copy prototype properties, // this is slower and exists for back-compat. Should be removed in 2.0. lang._mixin(nodeArray, nlp); // _NodeListCtor指向一個將array包裝成NodeList的函數 nodeArray._NodeListCtor = function(array){ // call without new operator to preserve back-compat behavior return nl(array); }; return nodeArray; };
可以看到如果isNew為false,那就對一個新的array對象進行擴展。
擴展的能力,便是直接在NodeList.prototype上增加的方法。大家直接看源碼和我的注釋即可。
1 // add array redirectors 2 forEach(["slice", "splice"], function(name){ 3 var f = ap[name]; 4 //Use a copy of the this array via this.slice() to allow .end() to work right in the splice case. 5 // CANNOT apply ._stash()/end() to splice since it currently modifies 6 // the existing this array -- it would break backward compatibility if we copy the array before 7 // the splice so that we can use .end(). So only doing the stash option to this._wrap for slice. 8 //類似於:this._wrap(this.slice(parameter), this); 9 nlp[name] = function(){ return this._wrap(f.apply(this, arguments), name == "slice" ? this : null); }; 10 }); 11 // concat should be here but some browsers with native NodeList have problems with it 12 13 // add array.js redirectors 14 forEach(["indexOf", "lastIndexOf", "every", "some"], function(name){ 15 var f = array[name]; 16 //類似於:dojo.indexOf(this, parameter) 17 nlp[name] = function(){ return f.apply(dojo, [this].concat(aps.call(arguments, 0))); }; 18 }); 19 20 lang.extend(NodeList, {//將屬性擴展至原型鏈 21 // copy the constructors 22 constructor: nl, 23 _NodeListCtor: nl, 24 toString: function(){ 25 // Array.prototype.toString can't be applied to objects, so we use join 26 return this.join(","); 27 }, 28 _stash: function(parent){//保存parent,parent應當也是nl的一個實例 29 // summary: 30 // private function to hold to a parent NodeList. end() to return the parent NodeList. 31 // 32 // example: 33 // How to make a `dojo/NodeList` method that only returns the third node in 34 // the dojo/NodeList but allows access to the original NodeList by using this._stash: 35 // | require(["dojo/query", "dojo/_base/lang", "dojo/NodeList", "dojo/NodeList-dom" 36 // | ], function(query, lang){ 37 // | lang.extend(NodeList, { 38 // | third: function(){ 39 // | var newNodeList = NodeList(this[2]); 40 // | return newNodeList._stash(this); 41 // | } 42 // | }); 43 // | // then see how _stash applies a sub-list, to be .end()'ed out of 44 // | query(".foo") 45 // | .third() 46 // | .addClass("thirdFoo") 47 // | .end() 48 // | // access to the orig .foo list 49 // | .removeClass("foo") 50 // | }); 51 // 52 this._parent = parent; 53 return this; // dojo/NodeList 54 }, 55 56 on: function(eventName, listener){//綁定事件 57 // summary: 58 // Listen for events on the nodes in the NodeList. Basic usage is: 59 // 60 // example: 61 // | require(["dojo/query" 62 // | ], function(query){ 63 // | query(".my-class").on("click", listener); 64 // This supports event delegation by using selectors as the first argument with the event names as 65 // pseudo selectors. For example: 66 // | query("#my-list").on("li:click", listener); 67 // This will listen for click events within `<li>` elements that are inside the `#my-list` element. 68 // Because on supports CSS selector syntax, we can use comma-delimited events as well: 69 // | query("#my-list").on("li button:mouseover, li:click", listener); 70 // | }); 71 var handles = this.map(function(node){ 72 return on(node, eventName, listener); // TODO: apply to the NodeList so the same selector engine is used for matches 73 }); 74 handles.remove = function(){ 75 for(var i = 0; i < handles.length; i++){ 76 handles[i].remove(); 77 } 78 }; 79 return handles; 80 }, 81 82 end: function(){//由當前的nl返回父nl 83 // summary: 84 // Ends use of the current `NodeList` by returning the previous NodeList 85 // that generated the current NodeList. 86 // description: 87 // Returns the `NodeList` that generated the current `NodeList`. If there 88 // is no parent NodeList, an empty NodeList is returned. 89 // example: 90 // | require(["dojo/query", "dojo/NodeList-dom" 91 // | ], function(query){ 92 // | query("a") 93 // | .filter(".disabled") 94 // | // operate on the anchors that only have a disabled class 95 // | .style("color", "grey") 96 // | .end() 97 // | // jump back to the list of anchors 98 // | .style(...) 99 // | }); 100 // 101 if(this._parent){ 102 return this._parent; 103 }else{ 104 //Just return empty list. 105 return new this._NodeListCtor(0); 106 } 107 }, 108 109 110 concat: function(item){ 111 // summary: 112 // Returns a new NodeList comprised of items in this NodeList 113 // as well as items passed in as parameters 114 // description: 115 // This method behaves exactly like the Array.concat method 116 // with the caveat that it returns a `NodeList` and not a 117 // raw Array. For more details, see the [Array.concat 118 // docs](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/concat) 119 // item: Object? 120 // Any number of optional parameters may be passed in to be 121 // spliced into the NodeList 122 123 //return this._wrap(apc.apply(this, arguments)); 124 // the line above won't work for the native NodeList, or for Dojo NodeLists either :-( 125 126 // implementation notes: 127 // Array.concat() doesn't recognize native NodeLists or Dojo NodeLists 128 // as arrays, and so does not inline them into a unioned array, but 129 // appends them as single entities. Both the original NodeList and the 130 // items passed in as parameters must be converted to raw Arrays 131 // and then the concatenation result may be re-_wrap()ed as a Dojo NodeList. 132 133 var t = aps.call(this, 0), 134 m = array.map(arguments, function(a){//array.concat方法不會將原始的NodeList和dojo的NodeList作為數組來處理,所以在這之前將他們轉化成普通的數組 135 return aps.call(a, 0); 136 }); 137 return this._wrap(apc.apply(t, m), this); // dojo/NodeList 138 }, 139 140 map: function(/*Function*/ func, /*Function?*/ obj){ 141 // summary: 142 // see `dojo/_base/array.map()`. The primary difference is that the acted-on 143 // array is implicitly this NodeList and the return is a 144 // NodeList (a subclass of Array) 145 return this._wrap(array.map(this, func, obj), this); // dojo/NodeList 146 }, 147 148 forEach: function(callback, thisObj){ 149 // summary: 150 // see `dojo/_base/array.forEach()`. The primary difference is that the acted-on 151 // array is implicitly this NodeList. If you want the option to break out 152 // of the forEach loop, use every() or some() instead. 153 forEach(this, callback, thisObj); 154 // non-standard return to allow easier chaining 155 return this; // dojo/NodeList 156 }, 157 filter: function(/*String|Function*/ filter){ 158 // summary: 159 // "masks" the built-in javascript filter() method (supported 160 // in Dojo via `dojo/_base/array.filter`) to support passing a simple 161 // string filter in addition to supporting filtering function 162 // objects. 163 // filter: 164 // If a string, a CSS rule like ".thinger" or "div > span". 165 // example: 166 // "regular" JS filter syntax as exposed in `dojo/_base/array.filter`: 167 // | require(["dojo/query", "dojo/NodeList-dom" 168 // | ], function(query){ 169 // | query("*").filter(function(item){ 170 // | // highlight every paragraph 171 // | return (item.nodeName == "p"); 172 // | }).style("backgroundColor", "yellow"); 173 // | }); 174 // example: 175 // the same filtering using a CSS selector 176 // | require(["dojo/query", "dojo/NodeList-dom" 177 // | ], function(query){ 178 // | query("*").filter("p").styles("backgroundColor", "yellow"); 179 // | }); 180 181 var a = arguments, items = this, start = 0; 182 if(typeof filter == "string"){ // inline'd type check 183 items = query._filterResult(this, a[0]);//如果filter是css選擇器,調用query的filter方法從已有集合中選擇合適的元素 184 if(a.length == 1){ 185 // if we only got a string query, pass back the filtered results 186 return items._stash(this); // dojo/NodeList 187 } 188 // if we got a callback, run it over the filtered items 189 start = 1; 190 } 191 //如果filter是函數,那就調用array的filter先過濾在包裝。 192 return this._wrap(array.filter(items, a[start], a[start + 1]), this); // dojo/NodeList 193 }, 194 instantiate: function(/*String|Object*/ declaredClass, /*Object?*/ properties){ 195 // summary: 196 // Create a new instance of a specified class, using the 197 // specified properties and each node in the NodeList as a 198 // srcNodeRef. 199 // example: 200 // Grabs all buttons in the page and converts them to dijit/form/Button's. 201 // | var buttons = query("button").instantiate(Button, {showLabel: true}); 202 //這個方法主要用於將原生dom元素實例化成dojo的dijit 203 var c = lang.isFunction(declaredClass) ? declaredClass : lang.getObject(declaredClass); 204 properties = properties || {}; 205 return this.forEach(function(node){ 206 new c(properties, node); 207 }); // dojo/NodeList 208 }, 209 at: function(/*===== index =====*/){ 210 // summary: 211 // Returns a new NodeList comprised of items in this NodeList 212 // at the given index or indices. 213 // 214 // index: Integer... 215 // One or more 0-based indices of items in the current 216 // NodeList. A negative index will start at the end of the 217 // list and go backwards. 218 // 219 // example: 220 // Shorten the list to the first, second, and third elements 221 // | require(["dojo/query" 222 // | ], function(query){ 223 // | query("a").at(0, 1, 2).forEach(fn); 224 // | }); 225 // 226 // example: 227 // Retrieve the first and last elements of a unordered list: 228 // | require(["dojo/query" 229 // | ], function(query){ 230 // | query("ul > li").at(0, -1).forEach(cb); 231 // | }); 232 // 233 // example: 234 // Do something for the first element only, but end() out back to 235 // the original list and continue chaining: 236 // | require(["dojo/query" 237 // | ], function(query){ 238 // | query("a").at(0).onclick(fn).end().forEach(function(n){ 239 // | console.log(n); // all anchors on the page. 240 // | }) 241 // | }); 242 //與array中的位置選擇器類似 243 244 var t = new this._NodeListCtor(0); 245 forEach(arguments, function(i){ 246 if(i < 0){ i = this.length + i; } 247 if(this[i]){ t.push(this[i]); } 248 }, this); 249 return t._stash(this); // dojo/NodeList 250 } 251 });
NodeList提供的很多操作,如:map、filter、concat等,都是借助原生的Array提供的相應方法,這些方法返回都是原生的array對象,所以需要對返回的array對象進行包裝。有趣的是NodeList提供end()可以回到原始的NodeList中。整個結構如下:

我們來看一下包裝函數:
nl._wrap = nlp._wrap = tnl; var tnl = function(/*Array*/ a, /*dojo/NodeList?*/ parent, /*Function?*/ NodeListCtor){ //將a包裝成NodeList var nodeList = new (NodeListCtor || this._NodeListCtor || nl)(a); //設置nodeList._parent = parent;方便在end函數中返回原始nodeList return parent ? nodeList._stash(parent) : nodeList; }; end: function(){//由最近的nl返回父nl if(this._parent){ return this._parent; }else{ return new this._NodeListCtor(0); } },
這就是dojo中NodeList的設計!
query模塊暴露的方法無非就是對選擇器引擎的調用,下面就比較簡單了。
function queryForEngine(engine, NodeList){ var query = function(/*String*/ query, /*String|DOMNode?*/ root){ if(typeof root == "string"){ root = dom.byId(root); if(!root){ return new NodeList([]); } } //使用選擇器引擎來查詢dom節點 var results = typeof query == "string" ? engine(query, root) : query ? (query.end && query.on) ? query : [query] : []; if(results.end && results.on){//有end和on方法則認為query已經是一個NodeList對象 // already wrapped return results; } return new NodeList(results); }; query.matches = engine.match || function(node, selector, root){ // summary: // Test to see if a node matches a selector return query.filter([node], selector, root).length > 0; }; // the engine provides a filtering function, use it to for matching query.filter = engine.filter || function(nodes, selector, root){ // summary: // Filters an array of nodes. Note that this does not guarantee to return a NodeList, just an array. return query(selector, root).filter(function(node){ return array.indexOf(nodes, node) > -1; }); }; if(typeof engine != "function"){ var search = engine.search; engine = function(selector, root){ // Slick does it backwards (or everyone else does it backwards, probably the latter) return search(root || document, selector); }; } return query; } var query = queryForEngine(defaultEngine, NodeList);
如果您覺得這篇文章對您有幫助,請不吝點擊推薦,您的鼓勵是我分享的動力!!!
