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

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

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

 

前记:

基于 jQuery 1.7.1 编写;之前的系列文章以“贴源码注释”的方式进行讲解,注释并不适合做大段的描述和排版;本节将尝试 锚点+按块分析+流程图 的方式,希望这样能增加更详细的描述,方便阅读理解和加深记忆。

 

核心函数 .domManip()

 

概述

关于insertAdjacentXXX

.domManip()定义

局部变量初始化

规避WebKit checked属性

支持参数为函数

转换HTML代码为DOM元素

执行回调函数插入DOM元素

从.domManip()学到的

 

概述

 

.domManip()是jQuery DOM操作的核心函数,为以下DOM操作方法提供支持:

    append/appendTo prepend/prependTo before/insertBefore after/insertAfter

.domManip()做了两部分工作:

1. 将args转换为DOM元素,并放在一个文档碎片中,调用jQuery.buildFragment和jQuery.clean实现

2. 执行callback,将DOM元素作为参数传入,由callback执行实际的插入操作

image

 

关于insertAdjacentXXX

 

在很多分析jQuery DOM操作的资料里都提到了insertAdjacentXXX,即insertAdjacentElement、insertAdjacentHTML、insertAdjacentText,这三个方法在指定的位置插入DOM元素、HTML代码、文本。看看它的语法:

object.insertAdjacentElement/HTML/Text( sWhere, oElement/sText )

在DOM元素object的位置sWhere处插入指定的元素oElement/sText,sWhere指定了插入位置,可选的值有:

可选值

功能

jQuery中的等价方法

beforeBegin

object之前

.before()

afterBegin

前置,作为object的第一个子元素

.prepend()

beforeEnd

追加,作为object的最后一个子元素

.append()

afterEnd

object之后

.after()

这些功能在jQuery中都有对应的接口。sWhere的可选值一开始给我的感觉是有些丑陋,但是细想,似乎这些候选值跟能说明插入操作的本质。简单测试了下,IE和Chrome是支持的,Firefox不支持,但insertAdjacentXXX毕竟没有标准化(There is no public standard that applies to this method),实现一个类似的接口也不复杂。在jQuery里,与insertAdjacentXXX类似的就是.domManip()。

 

.domManip()定义

 

   1:      domManip: function( args, table, callback ) {

 

args 待插入的DOM元素或HTML代码

table 是否需要修正tbody,这个变量是优化的结果

callback 回调函数,执行格式为callback.call( 目标元素即上下文, 待插入文档碎片/单个DOM元素 )

 

局部变量初始化

 

   2:          var results, first, fragment, parent,
   3:              value = args[0],
   4:              scripts = [];
   5:   

 

value 是第一个元素,后边只针对args[0]进行检测,意味着args中的元素必须是统一类型;

scripts 在jQuery.buildFragment中会用到,脚本的执行在.domManip()的最后一行代码;jQuery.buildFragment中调用jQuery.clean时将scripts作为参数传入;jQuery.clean如果遇到script标签,则将script放入scripts,条件是:标签名为script 并且 未指定type或type为text/javascript;即支持插入script标签并执行;外联脚本通过jQuery.ajax以同步阻塞的方式请求然后执行,内联脚本通过jQuery.globalEval执行。

 

规避WebKit checked属性

 

   6:          // We can't cloneNode fragments that contain checked, in WebKit
   7:          if ( !jQuery.support.checkClone && arguments.length === 3 && typeof value === "string" && rchecked.test( value ) ) {
   8:              return this.each(function() {
   9:                  jQuery(this).domManip( args, table, callback, true );
  10:              });
  11:          }
  12:   

 

在WebKit中,不能克隆包含了已选中多选按钮的文档碎片;看看if代码块需要满足的条件:

不能正确拷贝选中状态 + 参数个数为3 + value是字符串 + 已选中的多选/单选按钮

Chrome和Safari用的都是WebKit引擎,在Chrome下jQuery.support.checkClone是true,那么问题就在Safari中; 在each的回调函数中再次调用.domManip(),但是有4个参数(增加了最后一个true),为了使 arguments.length === 3 变为false么?

看不懂在搞什么!似乎早期的#bugid没有出现在jQuery注释中,不好查找原因,有待继续研究 TODO。

 

支持参数为函数

 

   13:          if ( jQuery.isFunction(value) ) {
   14:              return this.each(function(i) {
   15:                  var self = jQuery(this);
   16:                  args[0] = value.call(this, i, table ? self.html() : undefined);
   17:                  self.domManip( args, table, callback );
   18:              });
   19:          }
   20:   

 

如果value是函数,则执行函数,并用返回的结果,再次调用domManip;执行value时,如果table为true则传入innerHTML,用来修正tbody;用value的返回值替换args[0],最后用修正过的args,迭代调用.domManip()。但是这里只处理args[0]是function的情况,如果args是function数组呢?验证一下:

$d = $('div'), i = 0, f = function() { return ++i };
$d.append( f, f, f ); // 只添加第一个,并非我所设想的会处理所有的待插入元素
$d.append( 1, 2, 3 ); // 添加3个

 

转换HTML代码为DOM元素

 

  21:          if ( this[0] ) {
  22:              parent = value && value.parentNode;
  23:   
  24:              // If we're in a fragment, just use that instead of building a new one
  25:              if ( jQuery.support.parentNode && parent && parent.nodeType === 11 && parent.childNodes.length === this.length ) {
  26:                  results = { fragment: parent };
  27:   
  28:              } else {
  29:                  results = jQuery.buildFragment( args, this, scripts );
  30:              }
  31:   
  32:              fragment = results.fragment;
  33:   
  34:              if ( fragment.childNodes.length === 1 ) {
  35:                  first = fragment = fragment.firstChild;
  36:              } else {
  37:                  first = fragment.firstChild;
  38:              }
  39:   

 

第25行:如果父元素是文档碎片DocumentFragment(nodeType === 11 ),那么不需要重新创建用现成的,否则就需要新建一个文档碎片;关于jQuery.support.parentNode,从字面上看应该是检测浏览器是否支持父元素属性parentNode,但是在jQuery的整篇源码中没有关于parentNode的检测,也就是说一直是undefined,我也很怀疑还有不支持父元素属性parentNode的浏览器吗?留给插件用么?

继续查资料,在DocumentFragment http://www.w3school.com.cn/xmldom/dom_documentfragment.asp 中有这样的说明:DocumentFragment 节点不属于文档树,继承的 parentNode 属性总是 null

所以这里应该是预留的检测:对DocumentFragment进行检测,因为DocumentFragment的parentNode总是null。

第29行:没有父元素或父元素不是文档碎片,则调用 jQuery.buildFragment 创建一个包含args的文档碎片,jQuery.buildFragment用到了缓存,重复的创建会被缓存下来(需满足一些条件讲到jQuery.buildFragment时会详细分析),jQuery.buildFragment返回的结构是 { fragment: fragment, cacheable: cacheable }

第34~38行:获取第一个子元素first,first在后边用于判断是否需要修正tr的父元素为tbody,后边的不需要判断么?看来默认以第一个元素为准;如果只有一个子元素,那么可以省掉文档碎片;这么做可以更快的插入元素,简单测试下,为了使子元素个数检测失效,将第34行改为:

   34:              if ( false && fragment.childNodes.length === 1 ) {

测试用例:

for( var i = 0; i < 10; i++ ) {
    $b = $('body').html('');
    console.time('fragment' + i);
    for( var i = 0; i < 5000; i++ ) $b.append( '<div>' );
    console.timeEnd('fragment' + i);
}

判断和不判断各执行10次取平均值(单位ms):

 

1

2

3

4

5

6

7

8

9

10

平均

判断

456

817

1077

546

544

416

536

515

343

328

557.8

不判断

760

671

592

640

1186

931

824

1028

885

842

835.9

可见如果文档碎片中只有一个子元素,插入子元素要比插入文档碎片稍快。

 

到这里准备工作完成了,即把args转换为DOM元素(准确的说是创建包含args的文档碎片),后边开始执行回调函数开始实际的插入操作。(前戏可真长)

 

执行回调函数插入DOM元素

 

  40:              if ( first ) {
  41:                  table = table && jQuery.nodeName( first, "tr" );
  42:   
  43:                  for ( var i = 0, l = this.length, lastIndex = l - 1; i < l; i++ ) {
  44:                      callback.call(
  45:                          table ?
  46:                              root(this[i], first) :
  47:                              this[i],
  48:                          // Make sure that we do not leak memory by inadvertently discarding
  49:                          // the original fragment (which might have attached data) instead of
  50:                          // using it; in addition, use the original fragment object for the last
  51:                          // item instead of first because it can end up being emptied incorrectly
  52:                          // in certain situations (Bug #8070).
  53:                          // Fragments from the fragment cache must always be cloned and never used
  54:                          // in place.
  55:                          results.cacheable || ( l > 1 && i < lastIndex ) ?
  56:                              jQuery.clone( fragment, true, true ) :
  57:                              fragment
  58:                      );
  59:                  }
  60:              }
  61:   

 

第40行:如果成功的创建了DOM元素,才有必要开始插入操作

第41行:tr的父元素是tbody,table指示是否需要修正tbody

第43行:遍历当前jQuery对象中的匹配元素,缓存this.length(可算开始干活了)

第44行:执行回调函数callback,格式为callback.call( 目标元素即上下文, 待插入文档碎片/单个DOM元素 )

第46行:如果是tr,修正目标元素即上下文

第48~54行:翻译原文注释:

当无意中丢弃原始文档碎片(碎片上可能已附加数据)而不是使用它时,确保不会泄漏内存;此外,对最后一个元素使用原始文档碎片,而不是第一个,因为它在某些情况下会被错误的清空。

使用文档碎片时,如果是 可缓存的 缓存命中(指从缓存中取到文档碎片),则总是克隆。

第一段参考bug列表理解:

    Bug#8070 http://bugs.jquery.com/ticket/8070

    Basically a recent optimization to the clone method with this commit makes wrong assumptions about the existance of getElementsByTagName on DocumentFragments.

    Bug#8052 http://bugs.jquery.com/ticket/8052#comment:4

    As the variables elem and clone both can be DocumentFragments it's not safe to call getElementsByTagName on them.

    Because according to the specification DocumentFragements don't implement this method.

可见是由于DocumentFragements可能没有实现getElementsByTagName,而jQuery错误的假设getElementsByTagName是可用的;在Sizzle中可以看到对getElementsByTagName的检测:typeof context.getElementsByTagName !== "undefined";这个问题在1.5rc1(1.5发行候选版本)中发现,随后的1.5中得到修复。

第55~57行:克隆文档碎片/单个DOM元素,克隆的条件:(可缓存的 或 缓存命中) 或者 this中有多个元素(需要多次用到fragment);我们先考虑不缓存的情况,同样忽略l>1,因为l必然大于1否则不会进入for循环;在遍历到最后一个元素之前,一直对fragment进行克隆,最后一个元素使用创建的fragment;这里的实现和官网API的描述正好相反(http://api.jquery.com/append/ If there is more than one target element, however, cloned copies of the inserted element will be created for each target after the first.);讲到jQuery.buildFragment时,会对DocumentFragment做更多的讨论。

 

执行脚本元素

 

   62:              if ( scripts.length ) {
   63:                  jQuery.each( scripts, evalScript );
   64:              }
   65:          }
   66:   

 

如果脚本数组scripts的长度大于0,则执行其中的脚本;在jQuery.clean中,如果遇到script标签则会放入脚本数组scripts中,例如:

$('div').append( '<script>alert(123);</script>' )

evalScript负责执行script元素,如果是外联脚本(即通过src引入),用jQuery.ajax同步请求src指定的地址并自动执行;如果是内联脚本(即写在script标签内),用jQuery.globalEval执行。

 

   67:          return this;
   68:      }

链式语法。

 

从.domManip()学到的

 

1. 如果遇到tr,需要处理tbody的问题

2. 如果插入多个元素时,将多个元素先插入一个文档碎片,然后将文档碎片一次性插入指定的元素和位置

3. 将HTML代码转换为DOM元素,可以将HTML代码赋值给一个DIV元素的innerHTML属性,然后取DIV元素的子元素,即可得到转换后的DOM元素

posted on 2012-01-10 17:14  nuysoft  阅读(...)  评论(...编辑  收藏

导航