[原创] jQuery源码分析-12 DOM操作-Manipulation-核心函数jQuery.clean()
作者:nuysoft/高云 QQ:47214707 Email:nuysoft@gmail.com
声明:本文为原创文章,如需转载,请注明来源并保留原文链接。
基于 jQuery 1.7.1 编写
核心函数 jQuery.clean()
概述
jQuery.clean( elems, context, fragment, scripts ) 把HTML代码转换为DOM元素。
实现的原理是创建一个临时div,然后将HTML代码复制给div的innerHTMLshuxing,浏览器会自动生成DOM元素,最后解析div的子元素。
如果遇到需要包裹的标签,为了能正确的创建DOM元素,先包裹必须的父标签(HTML代码),设置innerHTML生成DOM元素后,再剥去包裹的父元素;
如果遇到script标签,为了能执行脚本,设置innerHTML生成DOM元素后,将script元素放入脚本数组scripts,脚本的执行在.ddomManip()的最后。
函数定义
1: clean: function( elems, context, fragment, scripts ) {
elems 待转换HTML代码数组
context 文档对象,会调用context的createTextNode方法和createElement方法
fragment 文档碎片,在其上插入div,在div上设置innerHTML
scripts 脚本数组
修正文档对象context
2: var checkScriptType;
3:
4: context = context || document;
5:
6: // !context.createElement fails in IE with an error but returns typeof 'object'
7: if ( typeof context.createElement === "undefined" ) {
8: context = context.ownerDocument || context[0] && context[0].ownerDocument || document;
9: }
10:
修正context,后边会调用context的createTextNode方法和createElement方法
声明返回值
11: var ret = [], j;
12:
ret 转换后的DOM元素数组,返回值
j 循环变量,后文循环删除空tbody和修正defaultChecked时用到(把变量声明放在前边真是头疼)
遍历待转换数组
13: for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) {
14: if ( typeof elem === "number" ) {
15: elem += "";
16: }
17:
18: if ( !elem ) {
19: continue;
20: }
21:
for循环遍历elems,elems应该是数组(如果是字符串,那就一个字符一个字符的创建TextNode);
两个小技巧:在for表达式的第一部分定义elem,可以减少一行定义elem的代码;将数字转换为字符串,自增一个空字符串即可
将非法值(!elem)的检测前置是一个好习惯。
遇到HTML代码开始转换
22: // Convert html string into DOM nodes
23: if ( typeof elem === "string" ) {
如果elem是字符串(如果是数字已经被转换为字符串),即HTML代码,开始转换为DOM元素;非字符串将不做转换
不是标签,创建TextNode
24: if ( !rhtml.test( elem ) ) {
25: elem = context.createTextNode( elem );
如果不是HTML标签或特殊符号,则创建文本节点;rhtml = /<|&#?\w+;/;<是HTML标签的起始符号,&#?\w+则是特殊符号的起始符号,例如:
特殊符号 |
命名实体 |
十进制编码 |
特殊符号 |
命名实体 |
十进制编码 |
特殊符号 |
命名实体 |
十进制编码 |
空格 |
|
  |
< |
< |
< |
> |
> |
> |
" |
" |
" |
× |
× |
÷ |
÷ |
||
± |
± |
± |
& |
& |
||||
© 带圈的C |
© |
© |
® 带圈的R |
® |
® |
更多请参考:
http://baike.baidu.com/view/368156.html#6
http://baike.baidu.com/view/383720.html#5_3
http://www.cnblogs.com/cbcye/archive/2009/08/31/1557076.html
是标签,开始转换
修正XHTML风格的标签
26: } else {
27: // Fix "XHTML"-style tags in all browsers
28: elem = elem.replace(rxhtmlTag, "<$1></$2>");
29:
修正XHTML风格的标签,关键在正则: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" style="abc" custome="abc"/>'.replace( rxhtmlTag, '<$1></$2>' )
// "<div class="abc" style="abc" custome="abc"></div>"
// 3. 更复杂的:
'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"
正则中的i g | \w [] [^],请参考正则基础资料,也可以将来看看本系列第二章正则表达式(未完文),这里主要分析精髓之处:
1. 以<开头,以/>结尾,开头结尾的说法不准确,但是方便解释理解,例如上边的例子3,即使标签前后有其他字符,也同样准确修正
2. 在分组部分,首先过滤掉不需要修正的标签:area|br|col|embed|hr|img|input|link|meta|param,(?!p)是反前向声明,要求接下来的字符不与模式p匹配,例如:'<areadiv/>'.replace( rxhtmlTag, '<$1></$2>' ) // "<areadiv/>"
3. 然后是精妙的分组嵌套(([\w:]+)[^>]*),将</>包围的字符作为第一个分组$1,标签([\w:]+)作为第二个分组$2,保留了HTML属性,同时提取标签,对于初学正则的我来说感觉就是出神入化
再回头看前边的三个例子,从简单到复杂,测试了这些功能:XHTML标签修正、保留HTML属性、过滤不需要修正的标签、正确修正未知标签、忽略大小写、多行匹配。
评价:优雅 风骚,读者一定要亲自测试。
创建临时div,插入到安全文档碎片
30: // Trim whitespace, otherwise indexOf won't work as expected
31: var tag = ( rtagName.exec( elem ) || ["", ""] )[1].toLowerCase(),
32: wrap = wrapMap[ tag ] || wrapMap._default,
33: depth = wrap[0],
34: div = context.createElement("div");
35:
tag 从HTML代码中提出出来的标签,原文注释是删除空白符,否则indexOf不能正常工作,可问题是后边没有调用indexOf,所以rtagName的功能应该是提取标签:rtagName = /<([\w:]+)/;
wrap 数组,其中放有tag的深度、父标签、父关闭标签,例如option对应 [ 1, "<select multiple='multiple'>", "</select>" ];这里可能有个问题,假设<td>td</td><div>div</div>,tag解析为td,然后加上包裹标签<table><tbody><tr>,把div也包了进去,因此要创建的元素必须是同级元素,至少是可以同级的元素,要么都不需要被包裹,要么一起被包裹
depth 深度,包裹了几层,例如option是1、tr是2、td是3,默认0即不包裹
div 创建一个临时div容器,后边在该div上设置innerHTML
36: // Append wrapper element to unknown element safe doc fragment
37: if ( context === document ) {
38: // Use the fragment we've already created for this document
39: safeFragment.appendChild( div );
40: } else {
41: // Use a fragment created with the owner document
42: createSafeFragment( context ).appendChild( div );
43: }
44:
在安全文档碎片上添加临时容器元素div;如果是当前主document(document在jQuery初始化的第一行代码赋值),则使用默认的已创建的safeFragment(通过document.createDocumentFragment()创建);否则使用元素所在的文档context创建一个安全的文档片段;这里所谓“安全”是指对HTML5标签的支持,由全局函数createSafeFragment( document )实现,HTML5新增标签在nodeNames中定义,函数createSafeFragment( document ) 逐个创建HTML5元素,教会CSS引擎渲染未知的HTML元素。
包裹HTML代码,设置innerHTML
45: // Go to html and back, then peel off extra wrappers
46: div.innerHTML = wrap[1] + elem + wrap[2];
47:
加上包裹标签,设置innerHTML,由浏览器生成DOM元素,比如 <option>1</option> 自动加上包裹标签,变成:<select multiple='multiple'><option>1</option></select>
如果包裹,剥去包裹元素
48: // Move to the right depth
49: while ( depth-- ) {
50: div = div.lastChild;
51: }
52:
剥皮,只有自动包裹了标签才会剥皮(IE是特列),自动包裹然后再取出来,比如option自动包裹上select,depth为1,div剥一层是select;也就是说while循环最后的结果是包含了elem的父元素,即div成了elem的父元素;比如tr,div变成tbody;td,div变为tr;thead/tfoot,div变为table,以此类推;即修正elem的父元素。
(吐槽一下:很巧妙的变量复用,但一个变量有多重含义和用途不是好习惯,如果不仔细分析谁能想像到变量div会从DIV元素变成另一个DOM元素呢?我赞成方法的复用,一个方法实现多个功能,比如读写共用一个方法、动态判断参数的个数和类型执行不同的逻辑;我反对在变量这种粒度上复用,因为看不懂、想不到;不过总体来说jQuery在代码复用上,做的还是非常棒)
移除IE自动插入的空tbody
53: // Remove IE's autoinserted <tbody> from table fragments
54: if ( !jQuery.support.tbody ) {
55:
56: // String was a <table>, *may* have spurious <tbody>
57: var hasBody = rtbody.test(elem),
58: tbody = tag === "table" && !hasBody ?
59: div.firstChild && div.firstChild.childNodes :
60:
61: // String was a bare <thead> or <tfoot>
62: wrap[1] === "<table>" && !hasBody ?
63: div.childNodes :
64: [];
65:
66: for ( j = tbody.length - 1; j >= 0 ; --j ) {
67: if ( jQuery.nodeName( tbody[ j ], "tbody" ) && !tbody[ j ].childNodes.length ) {
68: tbody[ j ].parentNode.removeChild( tbody[ j ] );
69: }
70: }
71: }
72:
if代码用于移除IE自动插入的空tbody;hasBody表示elem中是否含有tbody;
tag === "table" && !hasBody ? div.firstChild && div.firstChild.childNodes
如果是标签table,且elem中没有tbody标签,那么div.firstChild即table元素,取出table元素的所有子元素,其中含有tbody,还可能有thead/tfoot(注:后边的for循环只删除空tbody)
wrap[1] === "<table>" && !hasBody ? div.childNodes : []
如果父标签是table(包裹标签),且elem中没有tbody,即elem中是thead/tfoot,那么div是table元素,取table元素所有子元素,其中含有空tbody,和thead/ftoot。
然后在for循环中遍历取到的tbody(可能含有thead/tfoot),删除IE自动添加的空tbody(再次注意,非空tbody不会删除)
但是这个复合三元表达式并不像上边所说的这么简单,它其实是所有可能情况的高度提炼,我们把可能的状态列出来:
table 从elem解析出来的tag是否table |
tbody elem中是否含有tbody |
thead/ftoot 从elem解析出来的tag是否thead/ftoot |
|
- |
有 |
- |
只要elem中自带tbody,不需要删除,所以: [] |
有 |
无 |
无 |
变量div是DIV元素,firstChild是TABLE元素,IE会自动添加空tbody,所以: div.firstChild.childNodes |
无 |
无 |
有 |
变量div是TABLE元素,IE会自动添加空tbody,所以: div.childNodes |
无 |
无 |
无 |
如果elem是th/tr/td/col,jQuery自动插入tbody,变量div是TBODY元素,不需要删除;如果elem是其他标签,也不需要删除,所以: [] |
tbale、tbody、thead/tfoot各有2种状态,总计2*2*2=8种状态,因为table与thead/tfoot互斥,减1最后7种状态;再加上对elem可能是th/tr/td,最终状态很多;之前光这个复合三元就写了两页纸,隔了一周再看,才整理成上边的表格,才算弄明白。
插入IE自动剔除的空白符
73: // IE completely kills leading whitespace when innerHTML is used
74: if ( !jQuery.support.leadingWhitespace && rleadingWhitespace.test( elem ) ) {
75: div.insertBefore( context.createTextNode( rleadingWhitespace.exec(elem)[0] ), div.firstChild );
76: }
77:
IE会自动剔除HTML代码头部的空白符,取出头部的空白符,创建TextNode然后手动插入;rleadingWhitespace = /^\s+/
取到创建的DOM元素
78: elem = div.childNodes;
79: }
80: }
81:
取div的子元素集赋值给elem,elem会被合并到返回值ret中;如果需要包裹,变量div是待转换元素的父元素,如果不需要包裹,变量div就是DIV元素,所以这里可以简洁的取childNodes。
elem是可能有不同含义的复用变量,坏习惯!
修正IE6/7中defaultChecked属性
82: // Resets defaultChecked for any radios and checkboxes
83: // about to be appended to the DOM in IE 6/7 (#8060)
84: var len;
85: if ( !jQuery.support.appendChecked ) {
86: if ( elem[0] && typeof (len = elem.length) === "number" ) {
87: for ( j = 0; j < len; j++ ) {
88: findInputs( elem[j] );
89: }
90: } else {
91: findInputs( elem );
92: }
93: }
94:
修正radios、checkboxes的defaultChecked属性,IE6/7中的毛病;
在函数 findInputs( elem ) 中找到input,然后 elem.defaultChecked = elem.checked
合并转换后的DOM元素
95: if ( elem.nodeType ) {
96: ret.push( elem );
97: } else {
98: ret = jQuery.merge( ret, elem );
99: }
100: }
101:
如果elem是DOM元素,直接push入ret,可能elem本来就是DOM元素不需要转换,或者由HTML代码转换的TextNode元素;否则elem含有多个元素,调用jQuery.merge( first, second )将elem合并到ret。
提取script元素
到这里对elems的遍历完成,后边是提取script元素
102: if ( fragment ) {
103: checkScriptType = function( elem ) {
104: return !elem.type || rscriptType.test( elem.type );
105: };
检测elem是否是script元素;rscriptType = /\/(java|ecma)script/i;rscriptType 标签script的type属性是否是javascript或ecmascript
106: for ( i = 0; ret[i]; i++ ) {
107: if ( scripts && jQuery.nodeName( ret[i], "script" ) && (!ret[i].type || ret[i].type.toLowerCase() === "text/javascript") ) {
108: scripts.push( ret[i].parentNode ? ret[i].parentNode.removeChild( ret[i] ) : ret[i] );
109:
110: }
如果 ret[i] 的标签名为script 并且 未指定type或type为text/javascript,放入scripts数组
110: else {
111: if ( ret[i].nodeType === 1 ) {
112: var jsTags = jQuery.grep( ret[i].getElementsByTagName( "script" ), checkScriptType );
113:
114: ret.splice.apply( ret, [i + 1, 0].concat( jsTags ) );
115: }
116: fragment.appendChild( ret[i] );
117: }
118: }
119: }
120:
取出ret[i]下可能有的的script元素,调用jQuery.grep( elems, callback, inv )过滤,返回其中 未指定type或type包含/javascript或/ecmascript 的script元素数组jsTags,将这个数组插入ret[i]之后,下次循环时检查。
为什么要将script元素放入scripts数组,然后手动执行呢?因为将script标签直接设置innerHTML,脚本不会执行,例如:
$('body').empty()[0].innerHTML = '<script>alert(1)</script>' // 不会alert
还有个疑问,如果不是内联脚本而是外联脚本,即通过src引入外部JS文件,怎么执行呢?在evalScript( i, elem )会通过jQuery.ajax以同步阻塞的方式请求src然后执行返回的脚本。
另学习两个技巧:
1. apply,利用apply的第二个参数是数组的特性,将jsTags插入ret中,注意appy与call的区别,以下的三行代码是等价的:
foo.call(this, arg1,arg2,arg3)
foo.apply(this, arguments)
this.foo(arg1, arg2, arg3)
2. i + 1,将找到的jsTags通过splice插入ret[i],for循环下次遍历ret时,从jsTags开始遍历;splice()方法用于插入、删除或替换数组的元素。
还要注意这里对属性type值的检查,在if条件中判断type是否是text/javascript,而在checkScriptType中匹配的是含有/javascript或含有/ecmascript,但是只有text/javascript才会放入scripts数组,含有/ecmascript以及非text/javascript的script元素则不会放入scripts数组,当然也不会被执行。这是为什么呢?翻了下 http://www.w3school.com.cn/tags/att_script_type.asp ,原来script标签的type只能是text/javascript。那么这里if和checkScriptType的差别是必须么?看不懂有待继续研究。
返回转换后的DOM元素数组
121: return ret;
122: },
后记
DOM操作的核心方法/函数有三个:.domManip( args, table, callback )、jQuery.buildFragment ( args, nodes, scripts )、jQuery.clean( elems, context, fragment, scripts ),它们的原理、细节到此分析完毕,这三个核心方法/函数没有公开,也没有完整系统的参考文档,要细细的分析、推理、查证、试验,难免有错误、遗漏的地方,文中还有些遗留的没搞懂的问题,希望能一起讨论、学习。