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。


define(["../has", "require"],
        function(has, require){

"use strict";
var testDiv = document.createElement("div");
has.add("dom-qsa2.1", !!testDiv.querySelectorAll);
has.add("dom-qsa3", function(){
            // test to see if we have a reasonable native selector engine available
            try{
                testDiv.innerHTML = "<p class='TEST'></p>"; // test kind of from sizzle
                // Safari can't handle uppercase or unicode characters when
                // in quirks mode, IE8 can't handle pseudos like :empty
                return testDiv.querySelectorAll(".TEST:empty").length == 1;
            }catch(e){}
        });
var fullEngine;
var acme = "./acme", lite = "./lite";
return {
    // summary:
    //        This module handles loading the appropriate selector engine for the given browser

    load: function(id, parentRequire, loaded, config){
        var req = require;
        // here we implement the default logic for choosing a selector engine
        id = id == "default" ? has("config-selectorEngine") || "css3" : id;
        id = id == "css2" || id == "lite" ? lite :
                id == "css2.1" ? has("dom-qsa2.1") ? lite : acme :
                id == "css3" ? has("dom-qsa3") ? lite : acme :
                id == "acme" ? acme : (req = parentRequire) && id;
        if(id.charAt(id.length-1) == '?'){
            id = id.substring(0,id.length - 1);
            var optionalLoad = true;
        }
        // the query engine is optional, only load it if a native one is not available or existing one has not been loaded
        if(optionalLoad && (has("dom-compliant-qsa") || fullEngine)){
            return loaded(fullEngine);
        }
        // load the referenced selector engine
        req([id], function(engine){
            if(id != "./lite"){
                fullEngine = engine;
            }
            loaded(engine);
        });
    }
};
});

  选择器引擎的代码晦涩难懂,我们只需要关心最终暴露出来的接口的用法即可。

  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上增加的方法。大家直接看源码和我的注释即可。


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

  NodeList提供的很多操作,如:map、filter、concat等,都是借助原生的Array提供的相应方法,这些方法返回都是原生的array对象,所以需要对返回的array对象进行包装。有趣的是NodeList提供end()可以回到原始的NodeList中。整个结构如下:

  dojo/query源码解析

  我们来看一下包装函数:


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

如果您觉得这篇文章对您有帮助,请不吝点击推荐,您的鼓励是我分享的动力!!!