dojo/query源碼解析


dojo/query模塊是dojo為開發者提供的dom查詢接口。該模塊的輸出對象是一個使用css選擇符來查詢dom元素並返回NodeList對象的函數。同時,dojo/query模塊也是一個插件,開發者可以使用自定義的查詢引擎,query模塊會負責將引擎的查詢結果包裝成dojo自己的NodeList對象。

require(["dojo/query!sizzle"], function(query){
query("div")...

  要理解這個模塊就要搞清楚兩個問題:

  1. 如何查詢,查詢的原理?
  2. 查詢結果是什么,如何處理查詢結果?

  這兩個問題涉及到本文的兩個主題:選擇器引擎和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就是這樣來處理的(這里涉及到預編譯問題,看不明白的自動忽略即可。。。)

  
  dojo/query模塊的選擇器引擎通過dojo/selector/loader來加載。如果沒有在dojoConfig中配置 selectorEngine屬性,那么loader模塊會自己判斷使用acme和是lite引擎,原則是高版本瀏覽器盡量使用lite,而低版本盡量使用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 });
View Code

  選擇器引擎的代碼晦澀難懂,我們只需要關心最終暴露出來的接口的用法即可。

  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
  NodeList來自於原生DOM,是一系列dom節點的集合,一個類素組對象,MDN中的解釋:
  NodeList  objects are collections of nodes such as those returned by Node.childNodes and the document.querySelectorAll method.
  我們看到document.querySelectorAll方法返回一個NodeList對象,而且這個方法返回的NodeList對象是一個靜態的集合。
   In other cases, the 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,有的含蓄的實現如jQuery。
  要了解dojo中的NodeList需要把握以下規則:
  1. dojo中的NodeList就是擴展了能力的Array實例。所以需要一個函數將原生array包裝起來
  2. 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     });
View Code

  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);

  

  如果您覺得這篇文章對您有幫助,請不吝點擊推薦,您的鼓勵是我分享的動力!!!

 

 
 
 
 
 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM