您的位置:首页 > 产品设计 > UI/UE

dojo/query源码解析

2015-11-29 21:23 501 查看
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就是这样来处理的(这里涉及到预编译问题,看不明白的自动忽略即可。。。)

  
  dojo/query模块的选择器引擎通过dojo/selector/loader来加载。如果没有在dojoConfig中配置selectorEngine属性,那么loader模块会自己判断使用acme和是lite引擎,原则是高版本浏览器尽量使用lite,而低版本尽量使用acme。

// add array redirectors
forEach(["slice", "splice"], function(name){
var f = ap[name];
//Use a copy of the this array via this.slice() to allow .end() to work right in the splice case.
// CANNOT apply ._stash()/end() to splice since it currently modifies
// the existing this array -- it would break backward compatibility if we copy the array before
// the splice so that we can use .end(). So only doing the stash option to this._wrap for slice.
//类似于:this._wrap(this.slice(parameter), this);
nlp[name] = function(){ return this._wrap(f.apply(this, arguments), name == "slice" ? this : null); };
});
// concat should be here but some browsers with native NodeList have problems with it

// add array.js redirectors
forEach(["indexOf", "lastIndexOf", "every", "some"], function(name){
var f = array[name];
//类似于:dojo.indexOf(this, parameter)
nlp[name] = function(){ return f.apply(dojo, [this].concat(aps.call(arguments, 0))); };
});

lang.extend(NodeList, {//将属性扩展至原型链
// copy the constructors
constructor: nl,
_NodeListCtor: nl,
toString: function(){
// Array.prototype.toString can't be applied to objects, so we use join
return this.join(",");
},
_stash: function(parent){//保存parent,parent应当也是nl的一个实例
// summary:
//        private function to hold to a parent NodeList. end() to return the parent NodeList.
//
// example:
//        How to make a `dojo/NodeList` method that only returns the third node in
//        the dojo/NodeList but allows access to the original NodeList by using this._stash:
//    |    require(["dojo/query", "dojo/_base/lang", "dojo/NodeList", "dojo/NodeList-dom"
//    |    ], function(query, lang){
//    |        lang.extend(NodeList, {
//    |            third: function(){
//    |                var newNodeList = NodeList(this[2]);
//    |                return newNodeList._stash(this);
//    |            }
//    |        });
//    |        // then see how _stash applies a sub-list, to be .end()'ed out of
//    |        query(".foo")
//    |            .third()
//    |                .addClass("thirdFoo")
//    |            .end()
//    |            // access to the orig .foo list
//    |            .removeClass("foo")
//    |    });
//
this._parent = parent;
return this; // dojo/NodeList
},

on: function(eventName, listener){//绑定事件
// summary:
//        Listen for events on the nodes in the NodeList. Basic usage is:
//
// example:
//        |    require(["dojo/query"
//        |    ], function(query){
//        |        query(".my-class").on("click", listener);
//            This supports event delegation by using selectors as the first argument with the event names as
//            pseudo selectors. For example:
//        |         query("#my-list").on("li:click", listener);
//            This will listen for click events within `<li>` elements that are inside the `#my-list` element.
//            Because on supports CSS selector syntax, we can use comma-delimited events as well:
//        |         query("#my-list").on("li button:mouseover, li:click", listener);
//        |    });
var handles = this.map(function(node){
return on(node, eventName, listener); // TODO: apply to the NodeList so the same selector engine is used for matches
});
handles.remove = function(){
for(var i = 0; i < handles.length; i++){
handles[i].remove();
}
};
return handles;
},

end: function(){//由当前的nl返回父nl
// summary:
//        Ends use of the current `NodeList` by returning the previous NodeList
//        that generated the current NodeList.
// description:
//        Returns the `NodeList` that generated the current `NodeList`. If there
//        is no parent NodeList, an empty NodeList is returned.
// example:
//    |    require(["dojo/query", "dojo/NodeList-dom"
//    |    ], function(query){
//    |        query("a")
//    |            .filter(".disabled")
//    |                // operate on the anchors that only have a disabled class
//    |                .style("color", "grey")
//    |            .end()
//    |            // jump back to the list of anchors
//    |            .style(...)
//    |    });
//
if(this._parent){
return this._parent;
}else{
//Just return empty list.
return new this._NodeListCtor(0);
}
},

concat: function(item){
// summary:
//        Returns a new NodeList comprised of items in this NodeList
//        as well as items passed in as parameters
// description:
//        This method behaves exactly like the Array.concat method
//        with the caveat that it returns a `NodeList` and not a
//        raw Array. For more details, see the [Array.concat
//        docs](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/concat)
// item: Object?
//        Any number of optional parameters may be passed in to be
//        spliced into the NodeList

//return this._wrap(apc.apply(this, arguments));
// the line above won't work for the native NodeList, or for Dojo NodeLists either :-(

// implementation notes:
// Array.concat() doesn't recognize native NodeLists or Dojo NodeLists
// as arrays, and so does not inline them into a unioned array, but
// appends them as single entities. Both the original NodeList and the
// items passed in as parameters must be converted to raw Arrays
// and then the concatenation result may be re-_wrap()ed as a Dojo NodeList.

var t = aps.call(this, 0),
m = array.map(arguments, function(a){//array.concat方法不会将原始的NodeList和dojo的NodeList作为数组来处理,所以在这之前将他们转化成普通的数组
return aps.call(a, 0);
});
return this._wrap(apc.apply(t, m), this);    // dojo/NodeList
},

map: function(/*Function*/ func, /*Function?*/ obj){
// summary:
//        see `dojo/_base/array.map()`. The primary difference is that the acted-on
//        array is implicitly this NodeList and the return is a
//        NodeList (a subclass of Array)
return this._wrap(array.map(this, func, obj), this); // dojo/NodeList
},

forEach: function(callback, thisObj){
// summary:
//        see `dojo/_base/array.forEach()`. The primary difference is that the acted-on
//        array is implicitly this NodeList. If you want the option to break out
//        of the forEach loop, use every() or some() instead.
forEach(this, callback, thisObj);
// non-standard return to allow easier chaining
return this; // dojo/NodeList
},
filter: function(/*String|Function*/ filter){
// summary:
//        "masks" the built-in javascript filter() method (supported
//        in Dojo via `dojo/_base/array.filter`) to support passing a simple
//        string filter in addition to supporting filtering function
//        objects.
// filter:
//        If a string, a CSS rule like ".thinger" or "div > span".
// example:
//        "regular" JS filter syntax as exposed in `dojo/_base/array.filter`:
//        |    require(["dojo/query", "dojo/NodeList-dom"
//        |    ], function(query){
//        |        query("*").filter(function(item){
//        |            // highlight every paragraph
//        |            return (item.nodeName == "p");
//        |        }).style("backgroundColor", "yellow");
//        |    });
// example:
//        the same filtering using a CSS selector
//        |    require(["dojo/query", "dojo/NodeList-dom"
//        |    ], function(query){
//        |        query("*").filter("p").styles("backgroundColor", "yellow");
//        |    });

var a = arguments, items = this, start = 0;
if(typeof filter == "string"){ // inline'd type check
items = query._filterResult(this, a[0]);//如果filter是css选择器,调用query的filter方法从已有集合中选择合适的元素
if(a.length == 1){
// if we only got a string query, pass back the filtered results
return items._stash(this); // dojo/NodeList
}
// if we got a callback, run it over the filtered items
start = 1;
}
//如果filter是函数,那就调用array的filter先过滤在包装。
return this._wrap(array.filter(items, a[start], a[start + 1]), this);    // dojo/NodeList
},
instantiate: function(/*String|Object*/ declaredClass, /*Object?*/ properties){
// summary:
//        Create a new instance of a specified class, using the
//        specified properties and each node in the NodeList as a
//        srcNodeRef.
// example:
//        Grabs all buttons in the page and converts them to dijit/form/Button's.
//    |    var buttons = query("button").instantiate(Button, {showLabel: true});
//这个方法主要用于将原生dom元素实例化成dojo的dijit
var c = lang.isFunction(declaredClass) ? declaredClass : lang.getObject(declaredClass);
properties = properties || {};
return this.forEach(function(node){
new c(properties, node);
});    // dojo/NodeList
},
at: function(/*===== index =====*/){
// summary:
//        Returns a new NodeList comprised of items in this NodeList
//        at the given index or indices.
//
// index: Integer...
//        One or more 0-based indices of items in the current
//        NodeList. A negative index will start at the end of the
//        list and go backwards.
//
// example:
//    Shorten the list to the first, second, and third elements
//    |    require(["dojo/query"
//    |    ], function(query){
//    |        query("a").at(0, 1, 2).forEach(fn);
//    |    });
//
// example:
//    Retrieve the first and last elements of a unordered list:
//    |    require(["dojo/query"
//    |    ], function(query){
//    |        query("ul > li").at(0, -1).forEach(cb);
//    |    });
//
// example:
//    Do something for the first element only, but end() out back to
//    the original list and continue chaining:
//    |    require(["dojo/query"
//    |    ], function(query){
//    |        query("a").at(0).onclick(fn).end().forEach(function(n){
//    |            console.log(n); // all anchors on the page.
//    |    })
//    |    });
//与array中的位置选择器类似

var t = new this._NodeListCtor(0);
forEach(arguments, function(i){
if(i < 0){ i = this.length + i; }
if(this[i]){ t.push(this[i]); }
}, this);
return t._stash(this); // dojo/NodeList
}
});


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


  

  如果您觉得这篇文章对您有帮助,请不吝点击推荐,您的鼓励是我分享的动力!!!
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: