jQuery技术内幕:深入解析jQuery架构设计与实现原理. 2.3 jQuery.fn.init( selector, context, rootjQuery )-阿里云开发者社区

开发者社区> 华章出版社> 正文

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

简介:

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=/ onerror=alert(1)>”,并重新打开这个页面;此时$( location.hash )在执行时变为$('#<img src=/ onerror=alert(1)>')。在jQuery 1.6.3之前,“#<img src=/ onerror=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。

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

分享:

华章出版社

官方博客
官网链接