jQuery源码分析系列:.domManip() .buildFragment() .clean()

 

.domManip(args,table,callback):是jQuery DOM操作的核心函数,可以扩展出如下方法:

  append/appendTo:

  prepend/prependTo: 

  before/insertBefore:

  after/insertAfter:

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

2.执行回调函数插入DOM元素。进行callback,将DOM元素作为参数传入,callbacks执行实际操作。

 

扩展内容:

  insertAdjacentElement、insertAdjacentHTML、insertAdjacentText,在指定的位置插入DOM元素 HTML代码 文本。

insertAdjacentElement(sWhere,oElement/sText):在sWhere处插入oElement/sText内容。

可选值

功能

jQuery中的等价方法

beforeBegin

object之前

.before()

afterBegin

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

.prepend()

beforeEnd

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

.append()

afterEnd

object之后

.after()

domManip(args, table, callback)源码:

        执行步骤:
            1.转换HTML代码为DOM元素
            2.执行回调函数插入DOM元素

        代码流程结构:

            局部变量初始化->规避WebKit checked属性->支持参数为函数->转换HTML为DOM->执行回调函数插入DOM->执行脚本元素

        参数说明:
            args 待插入的DOM元素或HTML代码
            table 是否需要修正tbody 这个变量是优化用的
            callback  回调函数 执行格式为callback.call(目标元素即上下文,待插入文档碎片/单个DOM元素)

 1         domManip: function( args, table, callback ) {
 2             var results, first, fragment, parent,
 3                 value = args[0],//第一个元素,后边只针对args[0]进行检测
 4                 /*
 5                 在jQuery.buildFragment中会用到,脚本的执行在.domManip()的最后一行代码;jQuery.buildFragment中调用jQuery.clean时将
 6                 scripts作为参数传入;jQuery.clean如果遇到script标签,则将script放入scripts,条件是:标签名为script 并且 未指定type或type为text/javascript;
 7                 即支持插入script标签并执行;外联脚本通过jQuery.ajax以同步阻塞的方式请求然后执行,内联脚本通过jQuery.globalEval执行。
 8                 */
 9                 scripts = [];
10 
11             //规避WebKit checked属性 不能克隆包含了已选中多选按钮的文档碎片
12             //不能正确拷贝选中状态&&参数个数为3&&value(args[0])是字符串&&已选中的多选按钮 单选按钮
13             if ( !jQuery.support.checkClone && arguments.length === 3 && typeof value === "string" && rchecked.test( value ) ) {
14                 return this.each(function() {
15                     jQuery(this).domManip( args, table, callback, true );
16                 });
17             }
18 
19             //支持参数为函数  如果value为函数,则将value的执行结果作为args[0]的值  只能够处理一个
20             if ( jQuery.isFunction(value) ) {//value is function
21                 return this.each(function(i) {
22                     var self = jQuery(this);
23                     //执行函数并返回结果  如果table为true  调用innerHTML修正tbody,
24                     //用value的返回值替换args[0] 最后用修正过的args 迭代调用.domManip()
25                     args[0] = value.call(this, i, table ? self.html() : undefined);
26                     //再次调用domManip
27                     self.domManip( args, table, callback );
28                 });
29             }
30 
31             //将HTML转换成DOM
32             if ( this[0] ) {
33                 parent = value && value.parentNode;
34 
35                 //parent.nodeType === 11 检测父元素是文档碎片(DocumentFragment(nodeTYpe ===1)那么就不用重新创建文档碎片了
36                 if ( jQuery.support.parentNode && parent && parent.nodeType === 11 && parent.childNodes.length === this.length ) {
37                     results = { fragment: parent };//文档碎片就是现有的
38 
39                 } else {
40                     //没有父元素或父元素不是文档碎片,则调用 jQuery.buildFragment 创建一个包含args的文档碎片,jQuery.buildFragment
41                     //用到了缓存,重复的创建会被缓存下来(需满足一些条件讲到jQuery.buildFragment时会详细分析),
42                     //jQuery.buildFragment返回的结构是: { fragment: fragment, cacheable: cacheable }
43                     results = jQuery.buildFragment( args, this, scripts );
44                 }
45 
46                 fragment = results.fragment;//args
47 
48                 //获取第一个子元素first,first在后边用于判断是否需要修正tr的父元素为tbody。
49                 //以第一个元素为准;如果只有一个子元素,那么可以省掉文档碎片;这么做可以更快的插入元素,
50                 if ( fragment.childNodes.length === 1 ) {//childNodes:返回包含被选节点的子节点的 NodeList
51                     first = fragment = fragment.firstChild;
52                 } else {
53                     first = fragment.firstChild;
54                 }
55 
56                 //执行回调函数插入DOM元素
57                 if ( first ) {//如果成功创建了DOM元素
58                     //tr的父元素是tbody  table指示是否需要修正tbody
59                     table = table && jQuery.nodeName( first, "tr" );//节点有tr
60                     //遍历当前jQuery对象中的匹配元素,缓存this.length
61                     for ( var i = 0, l = this.length, lastIndex = l - 1; i < l; i++ ) {
62                         callback.call(//执行callback
63                             //如果是tr 修正目标元素即上下文
64                             table ?
65                                 /*
66                                 function root( elem, cur ) {//cur干了什么? 不解
67                                     return jQuery.nodeName(elem, "table") ?    //elem中是否有table标签
68                                         //返回第一个tbody或者创建一个tbody返回
69                                         (elem.getElementsByTagName("tbody")[0] ||
70                                         elem.appendChild(elem.ownerDocument.createElement("tbody"))) :
71                                         elem;
72                                 }
73                                 */
74                                 root(this[i], first) :    //如果是tr 修正目标元素即上下文
75                                 this[i],
76                             //克隆文档碎片  单个DOM元素  缓存的或者this中有多个元素;
77                             results.cacheable || (l > 1 && i < lastIndex) ?
78                                 //克隆
79                                 jQuery.clone( fragment, true, true ) :
80                                 fragment
81                         );
82                     }
83                 }
84                 //执行脚本元素  如果脚本数组scripts的长度大于0  则执行其中的脚本;  jQuery.clean中 如果script标签则会放入脚本数组scripts中
85                 //evalScript负责执行script元素,如果是外联脚本(即通过src引入 src =  " ... " ),用jQuery.ajax同步请求src指定的地址并自动执行;
86                 //如果是内联脚本(即写在script标签内),用jQuery.globalEval执行。
87                 if ( scripts.length ) {
88                     jQuery.each( scripts, evalScript );
89                 }
90             }
91             return this;
92         }

jQuery.buildFragment(args,nodes,scripts)源码分析:

    执行步骤:

        创建文档碎片
        转换HTML代码为DOM元素
        缓存

    执行流程:

        修正文档对象doc->是否符合缓存条件->创建文档碎片->转换HTML代码jQuery.clean()->文档碎片放入缓存->返回文档碎片和缓存状态

    关于文档碎片:

        DocumentFragment 接口表示文档的一部分(或一段)。更确切地说,它表示一个或多个邻接的 Document 节点和它们的所有子孙节点。
        DocumentFragment 节点不属于文档树,继承的 parentNode 属性总是 null。
        当请求把一个 DocumentFragment 节点插入文档树时,插入的不是 DocumentFragment 自身,而是它的所有子孙节点。
        这使得 DocumentFragment 成了有用的占位符,暂时存放那些一次插入文档的节点。

        可以用 Document.createDocumentFragment() 方法创建新的空 DocumentFragment 节点。
        也可以用 Range.extractContents() 方法 或 Range.cloneContents() 方法 获取包含现有文档的片段的 DocumentFragment 节点。

    results = jQuery.buildFragment( args, this, scripts );
    接受.domManip()传入的HTML代码,创建文档碎片DocumentFragment,然后调用jQuery.clean()在这个文档碎片上将HTML代码转换成DOM

    1.如果插入多个DOM元素,可以先将这些DOM插入到文档碎片,然后将文档碎片插入文档。这时插入的是文档碎片的子孙节点,可以提高性能。
    2.将重复的HTML代码转换为DOM元素,可以将转换后的DOM元素缓存起来,下次转换时,直接调用缓存。

    参数说明:

        args:待插入的DOM元素或HTML代码。
        nodes:jQuery对象,从其中获取Docuemnt对象,doc = nodes[0].ownerDocuemnt || nodes[0]
        scripts:脚本数组 依次传递 .domManip() -> jQuery.buildFragment() -> jQuery.clean()

 1 jQuery.buildFragment = function(args,nodes,scripts){
 2     var fragment,
 3         cacheable,
 4         cacheresults,
 5         doc,
 6         first = args[0];
 7 
 8     if(nodes && nodes[0]){
 9         //ownerDocument    返回节点所属的根元素
10         //第一个节点的根节点或者这个节点
11         doc = nodes[0].ownerDocument || nodes[0];
12     }
13     //如果没有createDocumentFragment属性,则doc为顶层文档
14     if(!doc.createDocumentFragment){
15         doc = document;
16     }
17 
18     //args长度等于1&&第一个元素时字符串&&字符串长度小于512&&主文档对象 && 第一个字符时左尖括号 && 支持缓存的标签(除了/<(?:script|object|embed|option|style)/i)
19     //&& 要么支持正确的checked属性赋值&&要么没有checked属性&&要么支持正确的checked属性赋值&&要么没有checked属性&&要么可以正确拷贝HTML5元素&&要么不是HTML5标签s
20     if(args.length === 1 && typeof first === "string" && first.length < 512 && doc === document && first.charAt(0) === "<"
21         && !rnocache.test(first) && (jQuery.support.checkClone || !rchecked.test(first)) &&
22         (jQuery.support.html5Clone || !rnoshimcache.test(first))){
23             //表示是否first满足缓存条件放入缓存
24             cacheable = true;
25             //从jQuery.fragments中查找缓存结果,jQuery.fragments是全局的文档碎片缓存对象
26             cacheresults = jQuery.fragments[first];
27             //缓存名中,并不是1  而是真正的文档碎片 , 1表示第一次调用jQuery.buildFragment时设置
28             if(cacheresults && cacheresults !== 1){
29                 fragment = cacheresults;
30             }
31     }
32     /*
33         1.不符合缓存条件
34         2.符合缓存条件第一次调用jQuery.buildFragment()此时缓存中还没有
35         3.符合缓存条件 第二次调用jQuery.buildFragment,此时缓存中是1
36     */
37     if(!fragment){
38         fragment = doc.createDocumentFragment();//创建一个文档碎片
39         jQuery.clean(args,doc,fragment,scripts);//调用jQuery.clean将HTML字符串args转换成DOM
40     }
41     /*
42         如果符合缓存条件,将文档碎片放入缓存,放入的可能是文档碎片fragment或1,取决于缓存中的已有值;
43         如果已有值为undefined则为1,如果是1则为fragment
44 
45         执行过程
46         第一次     设置为1,因为已有值为undefined
47         第二次    设置为fragment,已有值为1
48         第三次      开始取缓存中的值
49     */
50     if(cacheable){
51         jQuery.fragments[first] = cacheresults ? fragment : 1;
52     }
53     //返回文档碎片和缓存状态
54     return {fragment:fragment,cacheable:cacheable};
55 }

clean()源码分析:

         修正文档对象context
        声明返回值
        遍历带转换数组
            遇到HTML代码开始转换
                不是标签 创建TextNode
                是标签 开始转换
                    修正XHTML风格的标签
                    创建临时div 插入到安全文档碎片
                    包裹HTML代码 设置innerHTML
                    如果包裹 剥去包裹元素
                    移除IE自动插入的空tbody
                    插入IE自动剔除的空白符
                    取到创建的DOM元素
                修正IE6/7中defaultChecked属性
                合并转换后的DOM元素
        提取script元素
        返回转换后的DOM元素数组

        elems         待转换的HTML代码数组
        context     文档对象 会调用context的createTExtNode方法和createElement方法
        fragment    文档碎片 在其上插入div 在div上设置innerHTML
        scripts      脚本数组

  1     clean: function( elems, context, fragment, scripts ) {
  2         //修正文档对象context  后面会调用createTextNode方法和createElement方法
  3         var checkScriptType;
  4         context = context || document;
  5         if ( typeof context.createElement === "undefined" ) {
  6             context = context.ownerDocument || context[0] && context[0].ownerDocument || document;
  7         }
  8 
  9         //声明返回值:ret转换后的DOM元素数组 返回值
 10         var ret = [],
 11             j;//j循环变量 后文循环删除空tbody和修正defaultChecked时用到
 12         //遍历待转换的数组elems(如果是字符串,那就一个字符一个字符的创建TextNode)
 13         for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) {
 14             //将数字转换成字符串
 15             if ( typeof elem === "number" ) {
 16                 elem += "";
 17             }
 18             //检测非法值,
 19             if ( !elem ) {
 20                 continue;
 21             }
 22 
 23             //遇到HTML代码开始转换
 24             if ( typeof elem === "string" ) {
 25                 //如果不是标签,就创建TextNode 文本节点  rhtml = /<|&#?w+;/;<是HTML标签的起始符号,&#?w+则是特殊符号的起始符号
 26                 if ( !rhtml.test( elem ) ) {
 27                     elem = context.createTextNode( elem );
 28                 } else {//如果是标签,即使HTML代码
 29                     //修正XHTML风格的标签  :XHTML标签修正、保留HTML属性、过滤不需要修正的标签、正确修正未知标签、忽略大小写、多行匹配。
 30                     elem = elem.replace(rxhtmlTag, "<$1></$2>");
 31 
 32                     //创建临时DIV 插入到安全文档碎片
 33                     //从HTML代码中提出来的标签
 34                     var tag = (rtagName.exec( elem ) || ["", ""])[1].toLowerCase(),
 35                         //wrap数组,其中放有tag的深度,父标签,父关闭标签  例如option对应 [ 1, "<select multiple='multiple'>", "</select>" ];
 36                         wrap = wrapMap[ tag ] || wrapMap._default,
 37                         depth = wrap[0],//深度包裹了几层  例如option是1、tr是2、td是3,默认0即不包裹
 38                         //创建一个临时div容器 后边在改div上设置innerHTML
 39                         div = context.createElement("div");
 40 
 41                     //加上包裹标签,设置innerHTML,由浏览器生成DOM元素
 42                     //例如:<option>1</option> 自动加上包裹标签,变成:<select multiple='multiple'><option>1</option></select>
 43                     div.innerHTML = wrap[1] + elem + wrap[2];
 44 
 45                     //比如option自动包裹上select,depth为1,div剥一层是select;也就是说while循环最后的结果是包含了elem的父元素,即div成了elem的父元素;
 46                     //比如tr,div变成tbody;td,div变为tr;thead/tfoot,div变为table,以此类推;即修正elem的父元素。
 47                     while ( depth-- ) {
 48                         div = div.lastChild;
 49                     }
 50 
 51                     //移除IE自动插入的空tbody
 52                     if ( !jQuery.support.tbody ) {
 53                         // String was a <table>, *may* have spurious <tbody>
 54                         var hasBody = rtbody.test(elem),
 55                             tbody = tag === "table" && !hasBody ?
 56                                 div.firstChild && div.firstChild.childNodes :
 57 
 58                                 // String was a bare <thead> or <tfoot>
 59                                 wrap[1] === "<table>" && !hasBody ?
 60                                     div.childNodes :
 61                                     [];
 62 
 63                         for ( j = tbody.length - 1; j >= 0 ; --j ) {
 64                             if ( jQuery.nodeName( tbody[ j ], "tbody" ) && !tbody[ j ].childNodes.length ) {
 65                                 tbody[ j ].parentNode.removeChild( tbody[ j ] );
 66                             }
 67                         }
 68                     }
 69 
 70                     //插入IE自动剔除的空白符
 71                     if ( !jQuery.support.leadingWhitespace && rleadingWhitespace.test( elem ) ) {
 72                         div.insertBefore( context.createTextNode( rleadingWhitespace.exec(elem)[0] ), div.firstChild );
 73                     }
 74                     //取到创建的DOM元素 取div的子元素集赋值给elem,elem会被合并到分绘制ret中;
 75                     //如果需要包裹div是待转换元素的父元素,如果不需要包裹 变量div就是DIV元素。所以这里可以简洁地取childNodes
 76                     elem = div.childNodes;
 77 
 78                 }
 79             }
 80 
 81             // 修正radios checkboxes的defaultChecked属性 IE6/7的bug
 82             //在函数findInputs(elem)中找到 input 然后elem.defaultChecked = elem.checked
 83             var len;
 84             if ( !jQuery.support.appendChecked ) {
 85                 if ( elem[0] && typeof (len = elem.length) === "number" ) {
 86                     for ( j = 0; j < len; j++ ) {
 87                         findInputs( elem[j] );
 88                     }
 89                 } else {
 90                     findInputs( elem );
 91                 }
 92             }
 93 
 94             //合并转换后的DOM元素
 95             if ( elem.nodeType ) {//如果是DOM元素直接push,elem本身就是DOM元素
 96                 ret.push( elem );
 97             } else {//elem含有多个元素,合并
 98                 ret = jQuery.merge( ret, elem );
 99             }
100         }
101 
102         //提取script元素
103         if ( fragment ) {
104             //检测是否是scriptType元素 rscriptType = //(java|ecma)script/i;rscriptType 标签script的type属性是否是javascript或ecmascrip
105             checkScriptType = function( elem ) {
106                 return !elem.type || rscriptType.test( elem.type );
107             };
108             for ( i = 0; ret[i]; i++ ) {
109                 //如果ret[i]的标签名为script并且为指定type或type为text/javascript 放入scripts数组
110                 if ( scripts && jQuery.nodeName( ret[i], "script" ) && (!ret[i].type || ret[i].type.toLowerCase() === "text/javascript") ) {
111                     scripts.push( ret[i].parentNode ? ret[i].parentNode.removeChild( ret[i] ) : ret[i] );
112 
113                 } else {
114                     /*
115                         取出ret[i]下可能有的的script元素,调用jQuery.grep( elems, callback, inv )过滤,返回其中 未指定type或type包含/javascript或/ecmascript
116                         的script元素数组jsTags,将这个数组插入ret[i]之后,下次循环时检查。
117 
118                         i + 1,将找到的jsTags通过splice插入ret[i],for循环下次遍历ret时,从jsTags开始遍历;splice()方法用于插入、删除或替换数组的元素
119                     */
120                     if ( ret[i].nodeType === 1 ) {
121                         var jsTags = jQuery.grep( ret[i].getElementsByTagName( "script" ), checkScriptType );
122 
123                         ret.splice.apply( ret, [i + 1, 0].concat( jsTags ) );
124                     }
125                     fragment.appendChild( ret[i] );
126                 }
127             }
128         }
129         //返回转换后的DOM数组
130         return ret;
131     },
原文地址:https://www.cnblogs.com/colorstory/p/2623095.html