2.5 jQuery.clean( elems, context, fragment, scripts )
2.5.1 实现原理
方法jQuery.clean( elems, context, fragment, scripts )负责把HTML代码转换成DOM元素,并提取其中的script元素。该方法先创建一个临时的div元素,并将其插入一个安全文档片段中,然后把HTML代码赋值给div元素的innerHTML属性,浏览器会自动生成DOM元素,最后解析div元素的子元素得到转换后的DOM元素。
安全文档片段指能正确渲染HTML5元素的文档片段,通过在文档片段上创建 HTML5元素,可以教会浏览器正确地渲染HTML5元素,稍后的源码分析会介绍其实现过程。
如果HTML代码中含有需要包裹在父标签中的子标签,例如,子标签<option>需要包裹在父标签<select>中,方法jQuery.clean()会先在HTML代码的前后加上父标签和关闭标签,在设置临时div元素的innerHTML属性生成DOM元素后,再层层剥去包裹的父元素,取出HTML代码对应的DOM元素。
如果HTML代码中含有<script>标签,为了能执行 <script>标签所包含的JavaScript代码或引用的JavaScript文件,在设置临时div元素的innerHTML属性生成DOM元素后,方法jQuery.clean()会提取其中的script元素放入数组scripts。注意,将含有<script>标签的HTML代码设置给某个元素的innerHTML属性后,<script>标签所包含的JavaScript代码不会自动执行,所引用的JavaScript文件也不会加载和执行。在11.2.1节分析DOM操作的核心工具方法jQuery.fn.domManip()时会看到,在生成的DOM元素插入文档树后,数组scripts中的script元素会被逐个手动执行。
2.5.2 源码分析
方法jQuery.clean( elems, context, fragment, scripts )执行的8个关键步骤如下:
1)创建一个临时div元素,并插入一个安全文档片段中。
2)为HTML代码包裹必要的父标签,然后赋值给临时div元素的innerHTML属性,从而将HTML代码转换为DOM元素,之后再层层剥去包裹的父元素,得到转换后的DOM元素。
3)移除IE 6/7自动插入的空tbody元素,插入IE 6/7/8自动剔除的前导空白符。
4)取到转换后的DOM元素集合。
5)在IE 6/7中修正复选框和单选按钮的选中状态。
6)合并转换后的DOM元素。
7)如果传入了文档片段fragment,则提取所有合法的script元素存入数组scripts,并把其他元素插入文档片段fragment。
8)最后返回转换后的DOM元素数组。
下面来看看该方法的源码实现。
1.?定义jQuery.clean( elems, context, fragment, scripts )
相关代码如下所示:
6256 clean: function( elems, context, fragment, scripts ) {
第6256行:定义方法jQuery.clean( elems, context, fragment, scripts ),它接受4个参数:
参数elems:数组,包含了待转换的HTML代码。
参数context:文档对象,该参数在方法jQuery.buildFragment()中被修正为正确的文档对象(变量doc),稍后会调用它的方法createTextNode()创建文本节点、调用方法createElement()创建临时div元素。
参数fragment:文档片段,作为存放转换后的DOM元素的占位符,该参数在jQuery.buildFragment()中被创建。
参数scripts:数组,用于存放转换后的DOM元素中的script元素。
2.?修正文档对象context
相关代码如下所示:
6257 var checkScriptType;
6258
6259 context = context || document;
6260
6261 // !context.createElement fails in IE with an error but returns typeof 'object'
6262 if ( typeof context.createElement === "undefined" ) {
6263 context = context.ownerDocument || context[0] && context[0].owner Document || document;
6264 }
6265
第6258~6264行:修正文档对象context,与方法jQuery.buildFragment() 对文档对象doc 的修正类似,ownerDocument 表示了 DOM 元素所在的文档对象。如果文档对象context 没有 createElement 方法,则尝试读取context.ownerDocument或context[0].ownerDocument,如果都没有,默认为当前文档对象 document。
既然方法jQuery.buildFragment()已经谨慎地修正了文档对象doc,并传给了方法jQuery.clean(),那么这里为什么要再次做类似的修正呢?这是为了方便直接调用jQuery.clean()转换HTML代码为DOM元素。例如,在DOM操作模块的方法.before()和.after()中,将直接调用jQuery.clean()转换HTML代码为DOM元素,且只传入了待转换的HTML代码数组elems,而没有传入文档对象context、文档片段fragment和script元素数组scripts,相关代码如下所示:
// jQuery.fn.before()
5776 before: function() {
5782 var set = jQuery.clean( arguments );
// jQuery.fn.after()
5788 after: function() {
5795 set.push.apply( set, jQuery.clean(arguments) );
方法jQuery.fn.before()在每个匹配元素之前插入指定的节点,方法jQuery.fn.after()则在每个匹配的元素之后插入指定的节点,这两个方法将在11.2.4节和11.2.5节介绍和分析。
3.?遍历待转换的HTML代码数组elems
相关代码如下所示:
6266 var ret = [], j;
6267
6268 for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) {
6269 if ( typeof elem === "number" ) {
6270 elem += "";
6271 }
6272
6273 if ( !elem ) {
6274 continue;
6275 }
6276
6277 // Convert html string into DOM nodes
6278 if ( typeof elem === “string” ) {
第6266行:数组ret用于存放转换后的DOM元素。
第6268行:开始遍历待转换的HTML代码数组。
注意,这里有个减少代码量的小技巧,在for语句的第1部分,声明了循环变量elem,在for语句的第2部分取出elems[i]赋值给elem,并判断elem的有效性。传统的做法可能是比较循环变量i与elems.length,然后在for循环体中把elems[i]赋值给elem,再判断elem的有效性。但这里通过一条for语句完成了循环变量elem的定义、赋值和有效性判断,减少了代码量,很实用且不影响阅读,读者可以在自己的代码中尝试应用这种写法。
另外,判断elem的有效性时使用的是“!=”,这样可以同时过滤null和undefined,却又不会过滤整型数字0。
第6269~6271行:如果elem是数值型,通过让elem自加一个空字符串,把elem转换为字符串,这也是一个很实用的小技巧。后面第6280行的context.createTextNode()也可以支持参数为数值型,这里把数值型转换为字符串,是为了简化随后对elem有效性和类型的判断。
第6273行:如果!elem为true,即elem可以转换为false,那么跳过本次循环,执行下一次循环。这行代码用于过滤空字符串的情况。如果elem是整型数字0,因为在前面的代码中已经被转换成了字符串“0”,所以这里可以简单地判断!elem。
在上面的两个if语句中,即使if语句块中只有一行代码,仍然用花括号包裹起来,这是一个好的编码习惯,方便阅读和理解,避免潜在的错误。
第6278行:如果elem是字符串,即HTML代码,则开始转换HTML代码为DOM元素,这之后的if语句块是jQuery.clean()的核心代码。
(1)创建文本节点
如果HTML代码中不包含标签、字符代码和数字代码,则调用原生方法document.createTextNode()创建文本节点,相关代码如下所示:
6279 if ( !rhtml.test( elem ) ) {
6280 elem = context.createTextNode( elem );
6281 } else {
第6279行:用正则rhtml检测HTML代码中是否含有标签、字符代码或数字代码,该正则的定义代码如下:
5649 rhtml = /<|&#?\w+;/,
标签的特征字符是左尖括号“<”,字符代码的特征字符是“&”,数字代码的特征字符是“&#”。字符代码和数字代码是特殊符号的两种形式,常见的特殊符号与字符代码、数字代码的编码对照表如表2-3所示。
表2-3 常见特殊符号与字符代码、数字代码的编码对照表
特 殊 符 号 字 符 代 码 数 字 代 码 备 注
" " " 双引号
' ' ' 单引号
< < < 小于
> > > 大于
& & &
  空格
? © © 版权符号
? ® ® 注册符号
? ™ ™ 商标符号
完整的特殊符号编码对照表请访问:
http://www.w3.org/MarkUp/Guide/Advanced.html
http://www.w3schools.com/tags/ref_entities.asp
第6280行:原生方法document.createTextNode()用于创建文本节点,但是对于传给它的字符串参数不会做转义解析,也就是说,该方法不能正确地解析和创建包含了字符代码或数字代码的字符串,而浏览器的innerHTML机制则可以。例如,下面的代码将在页面中输出“?©”:
document.body.innerHTML = '©';
// 显示转义后的"@"
document.body.appendChild( document.createTextNode('©') );
// 显示原始字符串"©"
第6281行:从这行代码开始转换包含了标签、字符代码或数字代码的HTML代码。
(2)修正自关闭标签
相关代码如下所示:
6282 // Fix "XHTML"-style tags in all browsers
6283 elem = elem.replace(rxhtmlTag, "<$1></$2>");
6284
第6282~6283行:用正则rxhtmlTag匹配HTML代码中的自关闭标签,并通过方法replace()替换为成对的标签,例如,<div/>会被修正为<div></div>。自关闭标签是指没有对应的关闭标签,而是在标签的最后加一个"/"来关闭它,例如,<div/>。方法replace()的第二个参数中的$1和$2分别对应正则rxhtmlTag的第一个和第二个分组。
正则rxhtmlTag是修正自关闭标签的关键所在,它的定义代码如下:
5646 rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,
这个正则有些长,可以先用几个例子来测试一下它的功能。
例1 最简单的自关闭标签。
'<div/>'.replace( rxhtmlTag, '<$1></$2>' );
// 输出:<div></div>
例2 带有属性的自关闭标签。
'<div class="abc"/>'.replace( rxhtmlTag, '<$1></$2>' );
// 输出:<div class="abc"></div>
例3 多个标签组合、标签前后有其他字符、大写标签<A></A>、不需要关闭的标签<input>、多行标签\n。
'a<div/>a \t b<A/>b \n c<xyz/>c \n d<IMG/>d \r\n e<input>e'.replace( rxhtmlTag, '<$1></$2>' );
// 输出:"a<div></div>a \t b<A></A>b \n c<xyz></xyz>c \n d<IMG/>d \r\n e<input>e"
现在来看看正则rxhtmlTag的精髓之处。
首先过滤掉不需要修正的标签:(?!area|br|col|embed|hr|img|input|link|meta|param)。(?!p)是反前向声明,要求接下来的字符不与模式p匹配。如果HTML代码中出现了这些标签,则不做任何处理。例如:
'<areadiv/>'.replace( rxhtmlTag, '<$1></$2>' )
// 输出:<areadiv/>
然后是精妙的嵌套分组,巧妙地提取了标签,同时又保留了属性:<和/>包围的(([\w:]+)[^>]*)作为第一个分组,其中包含了标签和属性;嵌套的([\w:]+)作为第二个分组,其中只包含了标签。
关于正则表达式和方法replace()的基础知识,不熟悉的读者请查阅相关的书籍资料。
(3)创建一个临时div元素
相关代码如下所示:
6285 // Trim whitespace, otherwise indexOf won't work as expected
6286 var tag = ( rtagName.exec( elem ) || ["", ""] )[1].toLowerCase(),
6287 wrap = wrapMap[ tag ] || wrapMap._default,
6288 depth = wrap[0],
6289 div = context.createElement("div");
6290
第6286行:提取HTML代码中的标签部分,删除了前导空白符和左尖括号,并转换为小写赋值给变量wrap。正则rtagName的定义如下:
5647 rtagName = /<([\w:]+)/,
第6287行:从对象wrapMap中取出标签tag对应的父标签,它的定义和初始化代码如下:
5657 wrapMap = {
5658 option: [ 1, "<select multiple='multiple'>", "</select>" ],
5659 legend: [ 1, "<fieldset>", "</fieldset>" ],
5660 thead: [ 1, "<table>", "</table>" ],
5661 tr: [ 2, "<table><tbody>", "</tbody></table>" ],
5662 td: [ 3, "<table><tbody><tr>", "</tr></tbody></table>" ],
5663 col: [ 2, "<table><tbody></tbody><colgroup>", "</colgroup></table>" ],
5664 area: [ 1, "<map>", "</map>" ],
5665 _default: [ 0, "", "" ]
5666 },
5667 safeFragment = createSafeFragment( document );
5668
5669 wrapMap.optgroup = wrapMap.option;
5670 wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead;
5671 wrapMap.th = wrapMap.td;
5672
5673 // IE can't serialize <link> and <script> tags normally
5674 if ( !jQuery.support.htmlSerialize ) {
5675 wrapMap._default = [ 1, "div<div>", "</div>" ];
5676 }
在对象wrapMap中包含了以下标签:option、optgroup、legend、thead、tbody、tfoot、colgroup、caption、tr、td、th、col、area、_default,每个标签对应一个数组,数组中的元素依次是:包裹的深度、包裹的父标签、父标签对应的关闭标签,例如,标签option对应的父标签是select,包裹深度为1。HTML语法要求这些标签必须包含在其对应的父标签中,在随后设置临时div元素的innerHTML属性之前,会在便签前后自动加上父标签和关闭标签。
关于对象wrapMap,有几个有趣的地方需要注意一下:
标签option需要包含在多选的<select multiple='multiple'>中。因为如果包含在单选的<selector>中,创建的第一个option元素的selected属性会被浏览器默认设置为true;而如果包含在多选的<select multiple='multiple'>中,则不会被浏览器修改。可以用下面的代码验证:
// 创建一个临时 div
var div = document.createElement('div');
// 单选 <selector>,输出 true
div.innerHTML = '<select><option>0</option></select>';
console.log( div.getElementsByTagName( 'option' )[0].selected ); // true
// 多选 <select multiple="multiple">,输出 false
div.innerHTML = '<select multiple="multiple"><option>0</option></select>';
console.log( div.getElementsByTagName( 'option' )[0].selected ); // false
严格按照HTML语法为tr、td、th添加父标签tbody。虽然设置innerHTML后浏览器会为tr、td、th自动添加父元素tbody,但保持结构良好的HTML代码是个好习惯。
在IE 9以下的浏览器中,不能序列化标签<link>和<script>,即通过浏览器的innerHTML机制不能将其转换为对应的link元素和script元素,此时测试项jQuery.support.htmlSerialize为false。解决方案是在标签<link>和<script>外包裹一层元素再转换。包裹的元素定义在wrapMap._default中,_default默认为[0, "",""],如果jQuery.support.htmlSerialize为false,则会在第5675行被修正为[1, "div<div>",
"</div>"]。可以运行用下面的代码来验证,运行结果如图2-6所示。
// IE 9 以下的浏览器
// 创建一个临时 div
var div = document.createElement('div');
// link 不包裹元素,输出 0
div.innerHTML = '<link rel="stylesheet" type="text/css" href="test.css" />';
console.log( div.getElementsByTagName("link").length ); // 0
// link 包裹元素,输出 1
div.innerHTML = 'div<div><link rel="stylesheet" type="text/css" href="test.css" /> </div>';
console.log( div.getElementsByTagName("link").length ); // 1
// script不包裹元素,输出 0
div.innerHTML = '<script></script>';
console.log( div.getElementsByTagName("script").length ); // 0
// script 包裹元素,输出 1
div.innerHTML = 'div<div><script></script></div>';
console.log( div.getElementsByTagName("script").length ); // 1
图2-6 序列化标签<link>和<script>
对对象wrapMap和测试jQuery.support.htmlSerialize的分析到此为止,关于对象wrapMap中标签的含义和语法请参考相关的基础书籍,更多关于浏览器的测试和修正请参考第7章。
下面回到对方法jQuery.clean()源码的分析。
第6288行:取出被包裹的深度赋值给变量depth,稍后将依据该变量层层剥去包裹的父元素。
第6289行:创建一个临时div元素,稍后它会被添加到一个安全文档片段中,并且它的innerHTML属性会被设置为待转换的HTML代码。
(4)把临时div元素插入一个安全文档片段中
相关代码如下所示:
6291 // Append wrapper element to unknown element safe doc fragment
6292 if ( context === document ) {
6293 // Use the fragment we've already created for this document
6294 safeFragment.appendChild( div );
6295 } else {
6296 // Use a fragment created with the owner document
6297 createSafeFragment( context ).appendChild( div );
6298 }
6299
第6292~6298行:如果传入的文档对象context是当前文档对象,则把临时div元素插入已创建的安全文档片段safeFragment中;否则,调用函数createSafeFragment()在文档对象context上创建一个新的安全文档片段,然后插入临时div元素。
所谓“安全”,是指不支持HTML5的浏览器也能够正确地解析和渲染未知的HTML5标签,即能够正确地构建DOM树,并且可以为之设置样式。IE 9以下的浏览器不支持HTML5,如果遇到未知标签(如<article>),浏览器会向DOM树中插入一个没有子元素的空元素。针对这个问题有一个“莫名其妙”的解决方法,就是在使用未知标签之前调用document.createElement( '未知标签' )创建一个对应的DOM元素,这样就可以“教会”浏览器正确地解析和渲染这个未知标签。
安全文档片段safeFragment在jQuery初始化时由函数createSafeFragment()创建,相关代码如下所示:
5667 safeFragment = createSafeFragment( document );
5628 function createSafeFragment( document ) {
5629 var list = nodeNames.split( "|" ),
5630 safeFrag = document.createDocumentFragment();
5631
5632 if ( safeFrag.createElement ) {
5633 while ( list.length ) {
5634 safeFrag.createElement(
5635 list.pop()
5636 );
5637 }
5638 }
5639 return safeFrag;
5640 }
5642 var nodeNames = "abbr|article|aside|audio|canvas|datalist|details|figcaption|figure|footer|" +
5643 "header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",
变量nodeNames中存放了所有的HTML5标签,函数createSafeFragment()在传入的文档对象document上创建一个新的文档片段,然后在该文档片段上逐个创建HTML5元素,从而“教会”不支持HTML5的浏览器正确地解析和渲染HTML5标签。
(5)利用浏览器的innerHTML机制将HTML代码转换为DOM元素
先为HTML代码包裹必要的父标签,然后赋值给临时div元素的innerHTML属性,从而将HTML代码转换为DOM元素,之后再层层剥去包裹的父元素,得到转换后的DOM元素。相关代码如下所示:
6300 // Go to html and back, then peel off extra wrappers
6301 div.innerHTML = wrap[1] + elem + wrap[2];
6302
6303 // Move to the right depth
6304 while ( depth-- ) {
6305 div = div.lastChild;
6306 }
6307
第6300行:为HTML代码包裹必要的父标签,然后赋值给临时div元素的innerHTML属性,浏览器会自动生成DOM元素。
第6304~6306行:用while循环层层剥去包裹的父元素,最终变量div将指向HTML代码对应的DOM元素的父元素。例如,标签<option>会被标签<select>包裹,包裹深度为1,剥去一层后变量div指向了select元素。再例如,标签<td>会被<table><tbody><tr>包裹,包裹深度为3,剥去3层后变量div指向了tr元素。如果HTML代码不需要包裹父标签,则变量depth为0,不会进入while循环。
(6)移除IE 6/7自动插入的空tbody元素
相关代码如下所示:
6308 // Remove IE's autoinserted <tbody> from table fragments
6309 if ( !jQuery.support.tbody ) {
6310
6311 // String was a <table>, *may* have spurious <tbody>
6312 var hasBody = rtbody.test(elem),
6313 tbody = tag === "table" && !hasBody ?
6314 div.firstChild && div.firstChild.childNodes :
6315
6316 // String was a bare <thead> or <tfoot>
6317 wrap[1] === "<table>" && !hasBody ?
6318 div.childNodes :
6319 [];
6320
6321 for ( j = tbody.length - 1; j >= 0 ; --j ) {
6322 if ( jQuery.nodeName( tbody[ j ], "tbody" ) && !
tbody[ j ].childNodes.length ) {
6323 tbody[ j ].parentNode.removeChild( tbody[ j ] );
6324 }
6325 }
6326 }
6327
第6309行:在IE 6/7中,浏览器会为空table元素自动插入空tbody元素,此时测试项jQuery.support.tbody为false。空元素指没有子元素的元素。
第6312行:用正则rtbody检查HTML代码中是否含有tbody标签,该正则的定义代码如下:
5648 rtbody = /<tbody/i,
第6313~6319行:提取IE 6/7自动插入的空tbody元素。这个复合三元表达式有些复杂,下面逐段分解之。
1)如果执行“tag === "table"&&!hasBody ? div.firstChild && div.firstChild.childNodes:”表示HTML代码中含有table标签,没有tbody标签,浏览器生成DOM元素时可能自动插入空tbody元素。此时变量div指向div元素,div.firstChild指向table元素,div.firstChild.childNodes则是tbody、thead、tfoot、colgroup、caption的元素集合。
2)如果执行“wrap[1] ==="<table>"&& !hasBody ? div.childNodes:”表示为HTML代码包裹了父标签<table>,但是HTML代码中没有tbody标签,即HTML代码中含有thead、tfoot、colgroup、caption之一或多个,浏览器生成DOM元素时可能自动插入空tbody元素。此时变量div指向table元素,div.childNodes是tbody、thead、tfoot、colgroup、caption的元素集合。
3)[]。如果HTML代码中含有tbody标签,无论空或非空都不需要删除,所以是[]。在其他情况下,浏览器生成DOM元素时不会自动插入空tbody元素,仍然是[]。
第6321~6326行:遍历数组tbody,移除空tbody元素,非空tbody元素不会移除。
第6322行:因为数组tbody中的元素还可能是thead、tfoot、colgroup、caption元素,所以要先判断tbody[j]是否是tbody元素;因为前面“提取IE 6/7自动插入的空tbody元素”的代码已经覆盖了所有可能的情况,所以!tbody[j].childNodes.length属于防御性检查,以防万一。
(7)插入IE 6/7/8自动剔除的前导空白符
相关代码如下所示:
6328 // IE completely kills leading whitespace when innerHTML is used
6329 if ( !jQuery.support.leadingWhitespace && rleading Whitespace.test( elem ) ) {
6330 div.insertBefore( context.createTextNode( rleading
Whitespace.exec(elem)[0] ), div.firstChild );
6331 }
6332
第6329行:在IE 6/7/8中,设置innerHTML属性时,浏览器会自动剔除前导空白符,测试测试项jQuery.support.leadingWhitespace为false。用正则rleadingWhitespace检测HTML代码中是否含有前导空白符,该正则的定义代码如下所示:
5645 rleadingWhitespace = /^\s+/,
第6330行:用正则rleadingWhitespace提取HTML代码中的前导空白符,然后调用原生方法createTextNode()创建文本节点,最后插入div元素的第一个子元素前。
(8)取到转换后的DOM元素集合
相关代码如下所示:
6333 elem = div.childNodes;
6334 }
6335 }
6336
(9)在IE 6/7中修正复选框和单选按钮的选中状态
相关代码如下所示:
6337 // Resets defaultChecked for any radios and checkboxes
6338 // about to be appended to the DOM in IE 6/7 (#8060)
6339 var len;
6340 if ( !jQuery.support.appendChecked ) {
6341 if ( elem[0] && typeof (len = elem.length) === "number" ) {
6342 for ( j = 0; j < len; j++ ) {
6343 findInputs( elem[j] );
6344 }
6345 } else {
6346 findInputs( elem );
6347 }
6348 }
6349
第6340行:在IE 6/7 中,复选框和单选按钮插入DOM树后,其选中状态checked会丢失,此时测试项jQuery.support.appendChecked为false。通过在插入之前把属性 checked的值赋值给属性defaultChecked,可以解决这个问题。
第6341~6344行:遍历转换后的DOM元素集合,在每个元素上调用函数findInputs( elem )。函数findInputs( elem )会找出其中的复选框和单选按钮,并调用函数fixDefaultChecked( elem )把属性checked的值赋值给属性defaultChecked。
函数findInputs()和fixDefaultChecked()的相关代码如下所示:
6175 // Used in clean, fixes the defaultChecked property
6176 function fixDefaultChecked( elem ) {
6177 if ( elem.type === "checkbox" || elem.type === "radio" ) {
6178 elem.defaultChecked = elem.checked;
6179 }
6180 }
6181 // Finds all inputs and passes them to fixDefaultChecked
6182 function findInputs( elem ) {
6183 var nodeName = ( elem.nodeName || "" ).toLowerCase();
6184 if ( nodeName === "input" ) {
6185 fixDefaultChecked( elem );
6186 // Skip scripts, get other children
6187 } else if ( nodeName !== "script" && typeof elem.getElementsByTagName !== "undefined" ) {
6188 jQuery.grep( elem.getElementsByTagName("input"), fixDefaultChecked );
6189 }
6190 }
6191
(10)合并转换后的DOM元素
相关代码如下所示:
6350 if ( elem.nodeType ) {
6351 ret.push( elem );
6352 } else {
6353 ret = jQuery.merge( ret, elem );
6354 }
6355 }
6356
第6355行:到此,数组elems中的所有HTML代码都已转换为DOM元素,并合并到了数组ret中。
4.?传入文档片段fragment的情况
如果传入了文档片段fragment,则遍历数组ret,提取所有(包括子元素)合法的script元素存入数组scripts,并把其他元素插入文档片段fragment。相关代码如下所示:
6357 if ( fragment ) {
6358 checkScriptType = function( elem ) {
6359 return !elem.type || rscriptType.test( elem.type );
6360 };
6361 for ( i = 0; ret[i]; i++ ) {
6362 if ( scripts && jQuery.nodeName( ret[i], "script" ) && (!ret[i].type || ret[i].type.toLowerCase() === "text/javascript") ) {
6363 scripts.push( ret[i].parentNode ? ret[i].parentNode.remove Child( ret[i] ) : ret[i] );
6364
6365 } else {
6366 if ( ret[i].nodeType === 1 ) {
6367 var jsTags = jQuery.grep( ret[i].getElementsByTagName ( "script" ), checkScriptType );
6368
6369 ret.splice.apply( ret, [i + 1, 0].concat( jsTags ) );
6370 }
6371 fragment.appendChild( ret[i] );
6372 }
6373 }
6374 }
6375
第6358~6360行:初始化变量checkScriptType为一个函数,用于检测script元素是否是可执行。如果一个script元素没有指定属性type,或者属性type的值含有“/javascript”或“/ecmascript”,则认为是可执行的。正则rscriptType的定义代码如下:
5655 rscriptType = /\/(java|ecma)script/i,
第6361~6374行:遍历数组ret,提取所有(包括子元素)合法的script元素存入数组scripts,其他元素则插入文档片段fragment。
第6362~6363行:如果调用方法jQuery.clean()时传入了数组scripts,并找到了合法的script元素,则将该元素从其父元素中移除,然后存入数组scripts。如果一个script元素没有指定属性type,或者属性type的值是“text/javascript”,则认为是合法的。
通常应该为方法jQuery.clean()同时传入文档片段fragment和数组scripts。
第6366~6370行:在遍历数组ret的过程中,会提取当前元素所包含的script元素,并把其中可执行的插入数组ret,插入位置在当前元素之后,以便继续执行第6362~6363行的检测和提取。script元素是否可执行通过函数checkScriptType( elem )检测。
第6371行:在遍历数组ret的过程中,会把除了合法script元素之外的所有元素插入文档片段。
5.?返回转换后的DOM元素数组
相关代码如下所示:
6376 return ret;
6377 },
第6376行:最后,返回数组ret,其中包含了所有转换后的DOM元素。但是要注意,如果传入了文档片段fragment和数组scripts,那么调用jQuery.clean()的代码应该从文档片段fragment中读取转换后的DOM元素,并从数组scripts中读取合法的script元素;如果未传入,则只能使用返回值ret。
2.5.3 小结
方法jQuery.clean( elems, context, fragment, scripts )的执行过程可以总结为图2-7。
图2-7 jQuery.clean( elems, context, fragment, scripts )的执行过程
2.2~2.5节详细分析了构建 jQuery 对象的源码实现,从下一节开始,将介绍和分析其他的原型属性和方法,以及静态属性和方法,这些属性和方法是jQuery库的基础。