jQuery技术内幕:深入解析jQuery架构设计与实现原理.2.3 jQuery.fn.init( selector, context, rootjQuery )...

    xiaoxiao2024-02-02  154

    2.3 jQuery.fn.init( selector, context, rootjQuery )

    2.3.1 12个分支

    构造函数jQuery.fn.init()负责解析参数selector和context的类型,并执行相应的逻辑,最后返回jQuery.fn.init()的实例。参数selector和context共有12个有效分支,如表2-1所示。

    表2-1 参数selector和context的12个分支

             selector   context    示  例

    1       可以转换为false    —     $()

    2       DOM元素        —     $( document.body )

    3       字符串     “body”          —     $('body')

    4                单独标签         —     $('<div>')

    $('<div>',{'class': 'test'} )

    5                复杂 HTML 代码   —     $('<div>abc</div>')

    6                “#id”         undefined         $('#id')

    7                选择器表达式        undefined         $('div p')

    8                选择器表达式        jQuery 对象    $('div p', $('#id') )

    9                选择器表达式        DOM 元素      $('div.foo').click( function() {

       $('span', this ).addClass('bar');

    } );

    10     函数         —     $( function(){ ... } )

    11     jQuery 对象    —     $( $('div p') )

    12     其他任意类型的值         —     $( { abc: 123 } )

    $( [ 1, 2, 3 ] )

     

    下面分析jQuery.fn.init()的源码,看看它是如何解析和处理参数selector和context的12个分支的。

    2.3.2 源码分析

    1.?定义jQuery.fn.init( selector, context, rootjQuery )

    相关代码如下所示:

    99     init: function( selector, context, rootjQuery ) {

    100         var match, elem, ret, doc;

    101

    第99行:定义构造函数jQuery.fn.init( selector, context, rootjQuery ),它接受3个参数:

    参数 selector:可以是任意类型的值,但只有undefined、DOM 元素、字符串、函数、jQuery对象、普通 JavaScript对象这几种类型是有效的,其他类型的值也可以接受但没有意义。

    参数 context:可以不传入,或者传入DOM元素、jQuery对象、普通 JavaScript 对象之一。

    参数rootjQuery:包含了document对象的jQuery对象,用于 document.getElement

    ById()查找失败、selector是选择器表达式且未指定context、selector是函数的情况。rootjQuery 的定义和应用场景的代码如下所示:

    // document.getElementById() 查找失败

    172                       return rootjQuery.find( selector );

    // selector 是选择器表达式且未指定 context

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

    // selector 是函数

    198          return rootjQuery.ready( selector );

     

    // 定义 rootjQuery

    916 // All jQuery objects should point back to these

    917 rootjQuery = jQuery(document);

    918

    第100行:变量match、elem、ret、doc的功能会在接下来的分析过程中介绍。

    2.?参数selector可以转换为false

    参数selector可以转换为false,例如是undefined、空字符串、null等,则直接返回this,此时this是空jQuery对象,其属性length等于0。相关代码如下所示:

    102         // Handle $(""), $(null), or $( undefined )

    103         if ( !selector ) {

    104             return this;

    105         }

    106

    3.?参数selector是DOM元素

    如果参数selector有属性nodeType,则认为selector是DOM元素,手动设置第一个元素和属性context指向该DOM元素、属性length为1,然后返回包含了该DOM元素引用的jQuery对象。相关代码如下所示:

    107         // Handle $(DOMElement)

    108         if ( selector.nodeType ) {

    109             this.context = this[0] = selector;

    110             this.length = 1;

    111             return this;

    112         }

    113

    第108行:属性nodeType声明了文档树中节点的类型,例如,Element节点的该属性值是1,Text节点是3,Comment节点是9,Document对象是9,DocumentFragment节点是11。

    4.?参数selector是字符串“body”

    如果参数selector是字符串“body”,手动设置属性context指向document对象、第一个元素指向body元素、属性length为1,最后返回包含了body元素引用的jQuery对象。这里是对查找字符串“body”的优化,因为文档树中只会存在一个body元素。相关代码如下所示:

    114         // The body element only exists once, optimize finding it

    115         if ( selector === "body" && !context && document.body ) {

    116             this.context = document;

    117             this[0] = document.body;

    118             this.selector = selector;

    119             this.length = 1;

    120             return this;

    121         }

    122

    5.?参数selector是其他字符串

    如果参数selector是其他字符串,则先检测selector是HTML代码还是#id。相关代码如下所示:

    123         // Handle HTML strings

    124         if ( typeof selector === "string" ) {

    125             // Are we dealing with HTML string or an ID?

    126             if ( selector.charAt(0) === "<" && selector.charAt( selector.length - 1 ) === ">" && selector.length >= 3 ) {

    127                 // Assume that strings that start and end with <> are HTML and skip the regex check

    128                 match = [ null, selector, null ];

    129

    130             } else {

    131                 match = quickExpr.exec( selector );

    132             }

    133

    第126~128行:如果参数selector以“<”开头、以“>”结尾,且长度大于等于3,则假设这个字符串是HTML片段,跳过正则quickExpr的检查。注意这里仅仅是假设,并不一定表示它是真正合法的HTML代码,如“<div></p>”。

    第131行:否则,用正则quickExpr检测参数selector是否是稍微复杂一些的HTML代码(如“abc<div>”)或#id,匹配结果存放在数组match中。正则quickExpr的定义如下:

    39     // A simple way to check for HTML strings or ID strings

    40     // Prioritize #id over <tag> to avoid XSS via location.hash (#9521)

    41     quickExpr = /^(?:[^<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/,

    正则quickExpr包含两个分组,依次匹配HTML代码和id。如果匹配成功,则数组match的第一个元素为参数selector,第二个元素为匹配的HTML代码或undefined,第三个元素为匹配的id或undefined。下面的例子测试了正则quickExpr的功能:

    quickExpr.exec( '#target' );               // ["#target", undefined, "target"]

    quickExpr.exec( '<div>' );          // ["<div>", "<div>", undefined]

    quickExpr.exec( 'abc<div>' );             // ["abc<div>", "<div>", undefined]

    quickExpr.exec( 'abc<div>abc#id' ); // ["abc<div>abc#id", "<div>", undefined]

    quickExpr.exec( 'div' );                       // null

    quickExpr.exec( '<div><img></div>' );      // ["<div><img></div>", "<div><img>

    </div>", undefined]

    第41行黑底白字的,在jQuery 1.6.3和之后的版本中,为了避免基于location.hash的XSS攻击,于是在quickExpr中增加了。在jQuery 1.6.3之前的版本中quickExpr的定义如下:

    quickExpr = /^(?:[^<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/,

    在jQuery 1.6.3和之后的版本中,quickExpr匹配selector时如果遇到“#”,则认为不是HTML代码,而是#id,然后尝试调用document.getElementById()查找与之匹配的元素。而在jQuery 1.6.3之前的版本中,则只检查左尖括号和右尖括号,如果匹配则认为是HTML代码,并尝试创建DOM元素,这可能会导致恶意的XSS攻击。

    假设有下面的场景:

    在应用代码中出现$( location.hash ),即根据location.hash的值来执行不同的逻辑,而用户可以自行在浏览器地址栏中修改hash值为“#<img src=/ οnerrοr=alert(1)>”,并重新打开这个页面;此时$( location.hash )在执行时变为$('#<img src=/ οnerrοr=alert(1)>')。在jQuery 1.6.3之前,“#<img src=/ οnerrοr=alert(1)>”被认为是HTML代码并创建img元素,因为属性src指向的图片地址并不存在,事件句柄onerror被执行并弹出1。这样一来,攻击者就可以在事件句柄onerror中编写恶意的JavaScript代码,例如,读取用户cookie、发起Ajax请求等。

    读者可以访问以下地址,查看更多相关信息:

    http://bugs.jquery.com/ticket/9521

    http://ma.la/jquery_xss/

    (1)参数selector是单独标签

    如果参数selector是单独标签,则调用document.createElement()创建标签对应的DOM元素。相关代码如下所示:

    134             // Verify a match, and that no context was specified for #id

    135             if ( match && (match[1] || !context) ) {

    136

    137                 // HANDLE: $(html) -> $(array)

    138                 if ( match[1] ) {

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

    140                     doc = ( context ? context.ownerDocument || context : document );

    141

    142                     // If a single string is passed in and it's a single tag

    143                     // just do a createElement and skip the rest

    144                     ret = rsingleTag.exec( selector );

    145

    146                     if ( ret ) {

    147                         if ( jQuery.isPlainObject( context ) ) {

    148                             selector = [ document.createElement( ret[1] ) ];

    149                             jQuery.fn.attr.call( selector, context, true );

    150

    151                         } else {

    152                             selector = [ doc.createElement( ret[1] ) ];

    153                         }

    154

    第135行:检测正则quickExpr匹配参数selector的结果,如果match[1]不是undefined,即参数selector是HTML代码,或者match[2]不是undefined,即参数selector是#id,并且未传入参数context。这行代码利用布尔表达式的计算顺序,省略了对match[2]的判断,完整的表达式如下:

    if ( match && (match[1] || match[2] && !context) ) {

    如果match不是null且match[1]是undefined,那么此时match[2]必然不是undefined,所以对match[2]的判断可以省略。

    第138~140行:开始处理参数selector是HTML代码的情况,先修正context、doc,然后用正则rsingleTag检测HTML代码是否是单独标签,匹配结果存放在数组ret中。正则rsingleTag的定义如下:

    50     // Match a standalone tag

    51     rsingleTag = /^<(\w+)\s*\/?>(?:<\/\1>)?$/,

    正则rsingleTag包含一个分组“(\w+)”,该分组中不包含左右尖括号、不能包含属性、可以自关闭或不关闭;“\1”指向匹配的第一个分组“(\w+)”。

    第146~153行:如果数组ret不是null,则认为参数selector是单独标签,调用document.createElement()创建标签对应的DOM元素;如果参数context是普通对象,则调用jQuery方法.attr()并传入参数context,同时把参数context中的属性、事件设置到新创建的DOM元素上。

    之所以把创建的DOM元素放入数组中,是为了在后面第160行方便地调用jQuery.merge()方法。方法jQuery.merge()用于合并两个数组的元素到第一个数组,相关内容在2.8.8节介绍和分析。

    参数context的细节请参考2.1.1节;方法.attr()遇到特殊属性和事件类型属性时会执行同名的jQuery方法,相关内容将在8.2节介绍和分析;方法jQuery.isPlainObject()用于检测对象是否是“纯粹”的对象,即用对象直接量{}或new Object()创建的对象,这会在2.8.2节介绍和分析。

    (2)参数selector是复杂HTML代码

    如果参数selector是复杂HTML代码,则利用浏览器的innerHTML机制创建DOM元素。相关代码如下所示:

    155                     } else {

    156                        ret = jQuery.buildFragment( [ match[1] ], [ doc ] );

    157                        selector = ( ret.cacheable ? jQuery.clone(ret.fragment):ret.fragment ).childNodes;

    158                     }

    159

    160                     return jQuery.merge( this, selector );

    161

    第156行:创建过程由方法jQuery.buildFragment()和jQuery.clean()实现,方法jQuery.buildFragment()返回值的格式为:

    {

       fragment: 含有转换后的 DOM 元素的文档片段

       cacheable: HTML 代码是否满足缓存条件

    }

    第157行:如果HTML代码满足缓存条件,则在使用转换后的DOM元素时,必须先复制一份再使用,否则可以直接使用。

    方法jQuery.buildFragment()和jQuery.clean()将分别在2.4节和第2.5节中介绍和分析。

    第160行:将新创建的DOM元素数组合并到当前jQuery对象中并返回。

    (3)参数selector是“#id”,且未指定参数context

    如果参数selector是“#id”,且未指定参数context,则调用document.getElementById()查找含有指定id属性的DOM元素。相关代码如下所示:

    162                  // HANDLE: $("#id")

    163                  } else {

    164                     elem = document.getElementById( match[2] );

    165

    166                     // Check parentNode to catch when Blackberry 4.6 returns

    167                     // nodes that are no longer in the document #6963

    168                     if ( elem && elem.parentNode ) {

    169                         // Handle the case where IE and Opera return items

    170                         // by name instead of ID

    171                         if ( elem.id !== match[2] ) {

    172                             return rootjQuery.find( selector );

    173                         }

    174

    175                         // Otherwise, we inject the element directly into the jQuery object

    176                         this.length = 1;

    177                         this[0] = elem;

    178                     }

    179

    180                     this.context = document;

    181                     this.selector = selector;

    182                     return this;

    183                  }

    184

    第162~164行:如果参数selector是“#id”且未指定参数context,则调用document.getElementById()查找含有指定id属性的DOM元素。

    第166~168行:检查parentNode属性,因为Blackberry 4.6会返回已经不在文档中的DOM节点。

    第169~173行:如果所找到元素的属性id值与传入的值不相等,则调用Sizzle查找并返回一个含有选中元素的新jQuery对象。即使是document.getElementById()这样核心的方法也需要考虑浏览器兼容问题,在IE 6、IE 7、某些版本的Opera中,可能会按属性name查找而不是id。例如,下面的HTML代码,通过document.getElementById()并不能找到正确的DOM元素:

    <!DOCTYPE html>

    <html>

    <head>

       <meta http-equiv="Content-Type" content="text/html; charset=utf-8">

       <meta name="description" content="head meta description">

    </head>

    <body>

       <div id="description">

           body div description

       </div>

       <form name="divId">

           <div id="divId"></div>

       </form>

       <script>

           alert( document.getElementById( 'description' ).outerHTML );

           alert( document.getElementById( 'divId' ).outerHTML );

       </script>

    </body>

    </html>t

    在IE7中的运行结果如图2-2和图2-3所示。

    在这种情况下,Sizzle先通过document.getElementsByTagName("*")取出所有的DOM元素,然后检查每个元素的属性id是否与指定值相等,如果相等,则放入返回结果中。具体可查阅第3章关于“Sizzle”的介绍和分析。

     

    第175~182行:如果所找到元素的属性id值与传入的值相等,则设置第一个元素、属性length、context、selector,并返回当前jQuery对象。

    (4)参数selector是选择器表达式

    相关代码如下所示:

    185             // HANDLE: $(expr, $(...))

    186             } else if ( !context || context.jquery ) {

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

    188

    189             // HANDLE: $(expr, context)

    190             // (which is just equivalent to: $(context).find(expr)

    191             } else {

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

    193             }

    194

    此时依然在字符串分支中,参数selector不是单独标签、复杂HTML代码、#id,而是选择器表达式。如果没有指定上下文,则执行rootjQuery.find( selector );如果指定了上下文,且上下文是jQuery对象,则执行context.find( selector );如果指定了上下文,但上下文不是jQuery对象,则执行this.constructor( context ).find( selector ),即先创建一个包含了context的jQuery对象,然后在该jQuery对象上调用方法.find()。

    6.参数selector是函数

    相关代码如下所示:

    195         // HANDLE: $(function)

    196         // Shortcut for document ready

    197         } else if ( jQuery.isFunction( selector ) ) {

    198            return rootjQuery.ready( selector );

    199         }

    200

    第197~199行:如果参数selector是函数,则认为是绑定ready事件。从第198行代码可以看出$( function )是$( document ).ready( function )的简写。

    方法jQuery.isFunction()将在2.8.2节介绍和分析。

    7.?参数selector是jQuery对象

    相关代码如下所示:

    201         if ( selector.selector !== undefined ) {

    202             this.selector = selector.selector;

    203             this.context = selector.context;

    204         }

    205

    第201~204行:如果参数selector含有属性selector,则认为它是jQuery对象,将会复制它的属性selector和context。而且在紧随其后的第206行会把参数selector中包含的选中元素引用,全部复制到当前jQuery对象中。

    8.?参数selector是任意其他值

    相关代码如下所示:

    206         return jQuery.makeArray( selector, this );

    207     },

    第206行:如果selector是数组或伪数组(如jQuery对象),则都添加到当前jQuery对象中;如果selector是JavaScript对象,则作为第一个元素放入当前jQuery对象中;如果是其他类型的值,则作为第一个元素放入当前jQuery对象中。最后返回当前jQuery对象。

    2.3.3 小结

    至此,方法jQuery.fn.init( selector, context, rootjQuery )的12分支就介绍完了,相关的判断和执行过程可以整理为图2-4。

    相关资源:敏捷开发V1.0.pptx
    最新回复(0)