[原创] jQuery源码分析-12 DOM操作-Manipulation-核心函数jQuery.clean()

作者:nuysoft/高云 QQ:47214707 Email:nuysoft@gmail.com
声明:本文为原创文章,如需转载,请注明来源并保留原文链接。

jQuery源码分析系列(持续更新)

 

基于 jQuery 1.7.1 编写

 

核心函数 jQuery.clean()

 

概述

函数定义

修正文档对象context

声明返回值

遍历待转换数组

    遇到HTML代码开始转换

        不是标签,创建TextNode

        是标签,开始转换

            修正XHTML风格的标签

            创建临时div,插入到安全文档碎片

            包裹HTML代码,设置innerHTML

            如果包裹,剥去包裹元素

            移除IE自动插入的空tbody

            插入IE自动剔除的空白符

            取到创建的DOM元素

    修正IE6/7中defaultChecked属性

    合并转换后的DOM元素

提取script元素

返回转换后的DOM元素数组

后记

 

概述

 

jQuery.clean( elems, context, fragment, scripts ) 把HTML代码转换为DOM元素。

实现的原理是创建一个临时div,然后将HTML代码复制给div的innerHTMLshuxing,浏览器会自动生成DOM元素,最后解析div的子元素。

如果遇到需要包裹的标签,为了能正确的创建DOM元素,先包裹必须的父标签(HTML代码),设置innerHTML生成DOM元素后,再剥去包裹的父元素;

如果遇到script标签,为了能执行脚本,设置innerHTML生成DOM元素后,将script元素放入脚本数组scripts,脚本的执行在.ddomManip()的最后。

image

 

函数定义

 

   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+则是特殊符号的起始符号,例如:

特殊符号

命名实体

十进制编码

特殊符号

命名实体

十进制编码

特殊符号

命名实体

十进制编码

空格

&nbsp;

&#160;

<

&lt;

&#60;

>

&gt;

&#62;

"

&quot;

&#34;

×

&times

 

÷

&divide;

 

±

&plusmn;

&#177;

&

&amp;

       

© 带圈的C

&copy;

&#169;

® 带圈的R

&reg;

&#174;

     

更多请参考:

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 ),它们的原理、细节到此分析完毕,这三个核心方法/函数没有公开,也没有完整系统的参考文档,要细细的分析、推理、查证、试验,难免有错误、遗漏的地方,文中还有些遗留的没搞懂的问题,希望能一起讨论、学习。

posted on 2012-01-11 12:46  nuysoft  阅读(...)  评论(...编辑  收藏

导航