jQuery源码分析(十二): 选择器

前端基础 2016-11-14

起步

入口$提供css选择器有以下的处理方式:

$(document)
$('<div></div>')
$('div')
$('.class')
$('#id')
$('[attr]')  //[att=val] [att~=val] [att|=val] [attr^=val] [attr$=val] [attr*=val] [attribute!=value]
$()
$(function(){})
$("input:radio", $('div'))
$("<div>", {"class":"test", text:"ttt"})
$(".class1, .class2, div")
$("div > .class") // ancestor descendant,parent > child,prev + next,prev ~ siblings

针对选择器的处理太多了(肯定大于这12种),一个功能越灵活就意味着它的实现就越复杂。

选择器正则

jq构造器是jQuery.fn.init = function( selector, context ) {} 在这函数上面有个简单检测html字符串的表达式:

// A simple way to check for HTML strings
// Prioritize #id over <tag> to avoid XSS via location.hash (#9521)
// Strict HTML recognition (#11290: must start with <)
rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,

解析下这个表达式,/^express$/这表明字符串整个都要满足express。(?:pattern)匹配 pattern 但不获取匹配结果,匹配结果不进行存储也不提供以后使用。里面的表达式通过或"|"可以分为:

  • \s*(<[\w\W]+>)[^>]*:[\w\W]+ : 匹配于[A-Za-z0-9_][^A-Za-z0-9_] 一次或多次, 等价{1,};[^>]* : 负值字符集合,字符串尾部是除了>的任意字符或者没有字符,零次或多次等价于{0,},
  • #([\w-]*) 匹配结尾带上#号的任意字符,包括下划线与-

这个正则表达式虽然不能匹配上面12种用法的所有情况,但他能匹配html表达和id表达式

测试:

var rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/;
var selector = '<div id="top"></div>';
var match = rquickExpr.exec( selector );
console.log(match);//["<div id="top"></div>", "<div id="top"></div>", undefined, index:0, input:"<div id="top"></div>"]

(?:parttern)是不获取结果的啊,exec()不知道为什么会有值。难道js的不一样吗,不知道,先留着。

jQuery查询的的对象是dom元素,通过selector怎么找到目标元素的呢。

css选择器解析原理

这里有一个必须知道的真相,排版引擎解析css选择器的时候是从右往左搜索的。 尽量不要写成有很多层级关系的:

#div p.class {}

而是写成:

.class {}

按照思维的习惯,路径越清晰,查找的应该越快越精确的啊。但是事实上在css解析并不是这样的。

html文本经过解析生成dom树,css解析完毕后生成style rules或者说是css tree,需要将解析结果与dom树进行分拆建立一棵渲染数(Render Tree),用来最终的绘图。

如果是 从左往右 查找,例如[div div p .class],就要先找到最上层的div元素,如果不匹配就必须回到最上层div继续找,回溯若干次,效率较低。

如果是 从右往左 查找,只需找到em节点,不断向上找父节点验证即可。由于dom节点和css规则数量可能很大,所以不匹配的情况远远大于匹配。右往左进行解析还是会比从左往右解析要少很多次的匹配,这样带来的效率提升是显而易见的。

对象构造器

jq对象都是通过 $.fn.init 构造出来的:

jQuery.fn.init = function( selector, context ) {
    var match, elem;

    // HANDLE: $(""), $(null), $(undefined), $(false)
    if ( !selector ) {
        return this;
    }

    // Handle HTML strings
    if ( typeof selector === "string" ) {
        if ( selector[0] === "<" && selector[ selector.length - 1 ] === ">" && selector.length >= 3 ) {
            // Assume that strings that start and end with <> are HTML and skip the regex check
            match = [ null, selector, null ];

        } else {
            match = rquickExpr.exec( selector );
        }

        // Match html or make sure no context is specified for #id
        if ( match && (match[1] || !context) ) {

            // HANDLE: $(#id)
            } else {
                //处理$('#id')
            }

        // HANDLE: $(expr, $(...))
        } else if ( !context || context.jquery ) {
            return ( context || rootjQuery ).find( selector );

        // HANDLE: $(expr, context)
        // (which is just equivalent to: $(context).find(expr)
        } else {
            return this.constructor( context ).find( selector );
        }

    // HANDLE: $(DOMElement)
    } else if ( selector.nodeType ) {
        this.context = this[0] = selector;
        this.length = 1;
        return this;

    // HANDLE: $(function)
    // Shortcut for document ready
    } else if ( jQuery.isFunction( selector ) ) {
        return typeof rootjQuery.ready !== "undefined" ?
            rootjQuery.ready( selector ) :
            // Execute immediately if ready is not present
            selector( jQuery );
    }

    if ( selector.selector !== undefined ) {
        this.selector = selector.selector;
        this.context = selector.context;
    }

    return jQuery.makeArray( selector, this );
};

要处理的情况很多,有需要if else也是难免的,可以总结出:

// HANDLE: $(""), $(null), $(undefined), $(false)
if ( !selector ) {
    return this;
}

处理"",null,undefiend,false,返回this,不进行其他处理。 处理字符串类型。 处理$(dom)。 处理$(function(){})。

比较复杂的是处理字符串那段.

if ( typeof selector === "string" ) {
    if ( selector[0] === "<" && selector[ selector.length - 1 ] === ">" && selector.length >= 3 ) {
        match = [ null, selector, null ];

    } else {
        match = rquickExpr.exec( selector );
    }
    //其他代码
}

发现不是发现不是 "<"开始,">"结尾,如 $('<p id="test">My <em>new</em> text</p>')这种的情况如果selector是html标签组成的话,直接match = [ null, selector, null ];而不用正则检查。否则的话需要match = rquickExpr.exec( selector )

处理html字符串

如果匹配是html字符串,那么mathc[1]是存在的:

// HANDLE: $(html) -> $(array)
if ( match[1] ) {
    context = context instanceof jQuery ? context[0] : context;

    // Option to run scripts is true for back-compat
    // Intentionally let the error be thrown if parseHTML is not present
    jQuery.merge( this, jQuery.parseHTML(
        match[1],
        context && context.nodeType ? context.ownerDocument || context : document,
        true
    ) );

    // HANDLE: $(html, props)
    if ( rsingleTag.test( match[1] ) && jQuery.isPlainObject( context ) ) {
        for ( match in context ) {
            // Properties of context are called as methods if possible
            if ( jQuery.isFunction( this[ match ] ) ) {
                this[ match ]( context[ match ] );

            // ...and otherwise set as attributes
            } else {
                this.attr( match, context[ match ] );
            }
        }
    }

    return this;
} 

通过对html解析函数 jQuery.parseHTML() 创建dom对象return [ context.createElement( parsed[1] ) ];,这段就是处理(html)->(array)方式。

处理#id:

// HANDLE: $(#id)
elem = document.getElementById( match[2] );

// Support: Blackberry 4.6
// gEBID returns nodes no longer in the document (#6963)
if ( elem && elem.parentNode ) {
    // Inject the element directly into the jQuery object
    this.length = 1;
    this[0] = elem;
}

this.context = document;
this.selector = selector;
return this;

简简单单用getElementById获取到dom对象。this.length = 1;this[0] = elem;将它转为对象数组。

处理.class

return ( context || rootjQuery ).find( selector );

第二个参数 context

构造器function(selector, context){}的第二个参数context表示的是一个上下文对象。默认是document,某些情况是rootjQuery$(document)

当处理如$('.class', $('#id')) 的时候就会执行:

return this.constructor( context ).find( selector );

所以 $('.class', $('#id')) 其实等价于 #('#id').find('.class')

以及:

context = context instanceof jQuery ? context[0] : context;

jQuery.parseHTML

前面介绍处理html字符串时候有点简单。

jQuery.merge( this, jQuery.parseHTML(
    match[1],
    context && context.nodeType ? context.ownerDocument || context : document,
    true
) );

这里ownerDocument和 documentElement有什么不一样: ownerDocument 是dom对象的一个属性,返回的是某个元素的根节点文档对象:即document对象 documentElementdocument 对象的属性,返回文档的根节点,(<html>标签对应的dom对象)

jQuery.parseHTML的源码:

jQuery.parseHTML = function( data, context, keepScripts ) {
    if ( !data || typeof data !== "string" ) {
        return null;
    }
    if ( typeof context === "boolean" ) {
        keepScripts = context;
        context = false;
    }
    context = context || document;

    var parsed = rsingleTag.exec( data ),
        scripts = !keepScripts && [];

    // Single tag
    if ( parsed ) {
        return [ context.createElement( parsed[1] ) ];
    }

    parsed = jQuery.buildFragment( [ data ], context, scripts );

    if ( scripts && scripts.length ) {
        jQuery( scripts ).remove();
    }

    return jQuery.merge( [], parsed.childNodes );
};

其中

var rsingleTag = (/^<(\w+)\s*\/?>(?:<\/\1>|)$/);

这是一个匹配独立标签的正则表达式:

  • ^<(\w+)\s*\/?> : 以<开头,至少跟着一个字符和任意个空白字符,之后出现0或1次/>
  • (?:<\/\1>|)$ : 可以匹配<、一个/或者空白并以之为结尾

这样如果没有任何属性和子节点的字符串(比如'<html></html>'或者'<div></div>'这样)会通过正则的匹配,当通过正则的匹配后则会通过传入的上下文直接创建一个节点:

// Single tag
if ( parsed ) {
    return [ context.createElement( parsed[1] ) ];
}

没通过正则表达式的字符串,会通过创建一个 div 节点,然后将字符串以innerHTML植入:

parsed = jQuery.buildFragment( [ data ], context, scripts );

而在jQuery.buildFragment()中:

fragment = context.createDocumentFragment();
//中间代码
tmp = tmp || fragment.appendChild( context.createElement("div") );
tmp.innerHTML = wrap[ 1 ] + elem.replace( rxhtmlTag, "<$1></$2>" ) + wrap[ 2 ];

这仅仅当参数是单一标签的情况如$('<img />') $('<p></p>').jq会有原生的createElement()创建;如果是一个复杂的html字符串,就创建一个div节点,然后通过innerHTML插入到文档中。

总结

css选择器是 从右向左 进行解析的,jq的构造器对很多情况都做了处理,比较杂乱,要自己多多理解。


本文由 hongweipeng 创作,采用 知识共享署名 3.0,可自由转载、引用,但需署名作者且注明文章出处。

如果对您有用,您的支持将鼓励我继续创作!