jQuery源码分析系列:.domManip() .buildFragment() .clean()
2014-06-22 15:03
651 查看
.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内容。
domManip(args, table, callback)源码:
执行步骤:
1.转换HTML代码为DOM元素
2.执行回调函数插入DOM元素
代码流程结构:
局部变量初始化->规避WebKit checked属性->支持参数为函数->转换HTML为DOM->执行回调函数插入DOM->执行脚本元素
参数说明:
args 待插入的DOM元素或HTML代码
table 是否需要修正tbody 这个变量是优化用的
callback 回调函数 执行格式为callback.call(目标元素即上下文,待插入文档碎片/单个DOM元素)
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()
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 脚本数组
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() |
执行步骤:
1.转换HTML代码为DOM元素
2.执行回调函数插入DOM元素
代码流程结构:
局部变量初始化->规避WebKit checked属性->支持参数为函数->转换HTML为DOM->执行回调函数插入DOM->执行脚本元素
参数说明:
args 待插入的DOM元素或HTML代码
table 是否需要修正tbody 这个变量是优化用的
callback 回调函数 执行格式为callback.call(目标元素即上下文,待插入文档碎片/单个DOM元素)
domManip: function( args, table, callback ) { var results, first, fragment, parent, value = args[0],//第一个元素,后边只针对args[0]进行检测 /* 在jQuery.buildFragment中会用到,脚本的执行在.domManip()的最后一行代码;jQuery.buildFragment中调用jQuery.clean时将 scripts作为参数传入;jQuery.clean如果遇到script标签,则将script放入scripts,条件是:标签名为script 并且 未指定type或type为text/javascript; 即支持插入script标签并执行;外联脚本通过jQuery.ajax以同步阻塞的方式请求然后执行,内联脚本通过jQuery.globalEval执行。 */ scripts = []; //规避WebKit checked属性 不能克隆包含了已选中多选按钮的文档碎片 //不能正确拷贝选中状态&&参数个数为3&&value(args[0])是字符串&&已选中的多选按钮 单选按钮 if ( !jQuery.support.checkClone && arguments.length === 3 && typeof value === "string" && rchecked.test( value ) ) { return this.each(function() { jQuery(this).domManip( args, table, callback, true ); }); } //支持参数为函数 如果value为函数,则将value的执行结果作为args[0]的值 只能够处理一个 if ( jQuery.isFunction(value) ) {//value is function return this.each(function(i) { var self = jQuery(this); //执行函数并返回结果 如果table为true 调用innerHTML修正tbody, //用value的返回值替换args[0] 最后用修正过的args 迭代调用.domManip() args[0] = value.call(this, i, table ? self.html() : undefined); //再次调用domManip self.domManip( args, table, callback ); }); } //将HTML转换成DOM if ( this[0] ) { parent = value && value.parentNode; //parent.nodeType === 11 检测父元素是文档碎片(DocumentFragment(nodeTYpe ===1)那么就不用重新创建文档碎片了 if ( jQuery.support.parentNode && parent && parent.nodeType === 11 && parent.childNodes.length === this.length ) { results = { fragment: parent };//文档碎片就是现有的 } else { //没有父元素或父元素不是文档碎片,则调用 jQuery.buildFragment 创建一个包含args的文档碎片,jQuery.buildFragment //用到了缓存,重复的创建会被缓存下来(需满足一些条件讲到jQuery.buildFragment时会详细分析), //jQuery.buildFragment返回的结构是: { fragment: fragment, cacheable: cacheable } results = jQuery.buildFragment( args, this, scripts ); } fragment = results.fragment;//args //获取第一个子元素first,first在后边用于判断是否需要修正tr的父元素为tbody。 //以第一个元素为准;如果只有一个子元素,那么可以省掉文档碎片;这么做可以更快的插入元素, if ( fragment.childNodes.length === 1 ) {//childNodes:返回包含被选节点的子节点的 NodeList first = fragment = fragment.firstChild; } else { first = fragment.firstChild; } //执行回调函数插入DOM元素 if ( first ) {//如果成功创建了DOM元素 //tr的父元素是tbody table指示是否需要修正tbody table = table && jQuery.nodeName( first, "tr" );//节点有tr //遍历当前jQuery对象中的匹配元素,缓存this.length for ( var i = 0, l = this.length, lastIndex = l - 1; i < l; i++ ) { callback.call(//执行callback //如果是tr 修正目标元素即上下文 table ? /* function root( elem, cur ) {//cur干了什么? 不解 return jQuery.nodeName(elem, "table") ? //elem中是否有table标签 //返回第一个tbody或者创建一个tbody返回 (elem.getElementsByTagName("tbody")[0] || elem.appendChild(elem.ownerDocument.createElement("tbody"))) : elem; } */ root(this[i], first) : //如果是tr 修正目标元素即上下文 this[i], //克隆文档碎片 单个DOM元素 缓存的或者this中有多个元素; results.cacheable || (l > 1 && i < lastIndex) ? //克隆 jQuery.clone( fragment, true, true ) : fragment ); } } //执行脚本元素 如果脚本数组scripts的长度大于0 则执行其中的脚本; jQuery.clean中 如果script标签则会放入脚本数组scripts中 //evalScript负责执行script元素,如果是外联脚本(即通过src引入 src = " ... " ),用jQuery.ajax同步请求src指定的地址并自动执行; //如果是内联脚本(即写在script标签内),用jQuery.globalEval执行。 if ( scripts.length ) { jQuery.each( scripts, evalScript ); } } return this; }
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()
jQuery.buildFragment = function(args,nodes,scripts){ var fragment, cacheable, cacheresults, doc, first = args[0]; if(nodes && nodes[0]){ //ownerDocument 返回节点所属的根元素 //第一个节点的根节点或者这个节点 doc = nodes[0].ownerDocument || nodes[0]; } //如果没有createDocumentFragment属性,则doc为顶层文档 if(!doc.createDocumentFragment){ doc = document; } //args长度等于1&&第一个元素时字符串&&字符串长度小于512&&主文档对象 && 第一个字符时左尖括号 && 支持缓存的标签(除了/<(?:script|object|embed|option|style)/i) //&& 要么支持正确的checked属性赋值&&要么没有checked属性&&要么支持正确的checked属性赋值&&要么没有checked属性&&要么可以正确拷贝HTML5元素&&要么不是HTML5标签s if(args.length === 1 && typeof first === "string" && first.length < 512 && doc === document && first.charAt(0) === "<" && !rnocache.test(first) && (jQuery.support.checkClone || !rchecked.test(first)) && (jQuery.support.html5Clone || !rnoshimcache.test(first))){ //表示是否first满足缓存条件放入缓存 cacheable = true; //从jQuery.fragments中查找缓存结果,jQuery.fragments是全局的文档碎片缓存对象 cacheresults = jQuery.fragments[first]; //缓存名中,并不是1 而是真正的文档碎片 , 1表示第一次调用jQuery.buildFragment时设置 if(cacheresults && cacheresults !== 1){ fragment = cacheresults; } } /* 1.不符合缓存条件 2.符合缓存条件第一次调用jQuery.buildFragment()此时缓存中还没有 3.符合缓存条件 第二次调用jQuery.buildFragment,此时缓存中是1 */ if(!fragment){ fragment = doc.createDocumentFragment();//创建一个文档碎片 jQuery.clean(args,doc,fragment,scripts);//调用jQuery.clean将HTML字符串args转换成DOM } /* 如果符合缓存条件,将文档碎片放入缓存,放入的可能是文档碎片fragment或1,取决于缓存中的已有值; 如果已有值为undefined则为1,如果是1则为fragment 执行过程 第一次 设置为1,因为已有值为undefined 第二次 设置为fragment,已有值为1 第三次 开始取缓存中的值 */ if(cacheable){ jQuery.fragments[first] = cacheresults ? fragment : 1; } //返回文档碎片和缓存状态 return {fragment:fragment,cacheable:cacheable}; }
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 脚本数组
clean: function( elems, context, fragment, scripts ) { //修正文档对象context 后面会调用createTextNode方法和createElement方法 var checkScriptType; context = context || document; if ( typeof context.createElement === "undefined" ) { context = context.ownerDocument || context[0] && context[0].ownerDocument || document; } //声明返回值:ret转换后的DOM元素数组 返回值 var ret = [], j;//j循环变量 后文循环删除空tbody和修正defaultChecked时用到 //遍历待转换的数组elems(如果是字符串,那就一个字符一个字符的创建TextNode) for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) { //将数字转换成字符串 if ( typeof elem === "number" ) { elem += ""; } //检测非法值, if ( !elem ) { continue; } //遇到HTML代码开始转换 if ( typeof elem === "string" ) { //如果不是标签,就创建TextNode 文本节点 rhtml = /<|?\w+;/;<是HTML标签的起始符号,?\w+则是特殊符号的起始符号 if ( !rhtml.test( elem ) ) { elem = context.createTextNode( elem ); } else {//如果是标签,即使HTML代码 //修正XHTML风格的标签 :XHTML标签修正、保留HTML属性、过滤不需要修正的标签、正确修正未知标签、忽略大小写、多行匹配。 elem = elem.replace(rxhtmlTag, "<$1></$2>"); //创建临时DIV 插入到安全文档碎片 //从HTML代码中提出来的标签 var tag = (rtagName.exec( elem ) || ["", ""])[1].toLowerCase(), //wrap数组,其中放有tag的深度,父标签,父关闭标签 例如option对应 [ 1, "<select multiple='multiple'>", "</select>" ]; wrap = wrapMap[ tag ] || wrapMap._default, depth = wrap[0],//深度包裹了几层 例如option是1、tr是2、td是3,默认0即不包裹 //创建一个临时div容器 后边在改div上设置innerHTML div = context.createElement("div"); //加上包裹标签,设置innerHTML,由浏览器生成DOM元素 //例如:<option>1</option> 自动加上包裹标签,变成:<select multiple='multiple'><option>1</option></select> div.innerHTML = wrap[1] + elem + wrap[2]; //比如option自动包裹上select,depth为1,div剥一层是select;也就是说while循环最后的结果是包含了elem的父元素,即div成了elem的父元素; //比如tr,div变成tbody;td,div变为tr;thead/tfoot,div变为table,以此类推;即修正elem的父元素。 while ( depth-- ) { div = div.lastChild; } //移除IE自动插入的空tbody if ( !jQuery.support.tbody ) { // String was a <table>, *may* have spurious <tbody> var hasBody = rtbody.test(elem), tbody = tag === "table" && !hasBody ? div.firstChild && div.firstChild.childNodes : // String was a bare <thead> or <tfoot> wrap[1] === "<table>" && !hasBody ? div.childNodes : []; for ( j = tbody.length - 1; j >= 0 ; --j ) { if ( jQuery.nodeName( tbody[ j ], "tbody" ) && !tbody[ j ].childNodes.length ) { tbody[ j ].parentNode.removeChild( tbody[ j ] ); } } } //插入IE自动剔除的空白符 if ( !jQuery.support.leadingWhitespace && rleadingWhitespace.test( elem ) ) { div.insertBefore( context.createTextNode( rleadingWhitespace.exec(elem)[0] ), div.firstChild ); } //取到创建的DOM元素 取div的子元素集赋值给elem,elem会被合并到分绘制ret中; //如果需要包裹div是待转换元素的父元素,如果不需要包裹 变量div就是DIV元素。所以这里可以简洁地取childNodes elem = div.childNodes; } } // 修正radios checkboxes的defaultChecked属性 IE6/7的bug //在函数findInputs(elem)中找到 input 然后elem.defaultChecked = elem.checked var len; if ( !jQuery.support.appendChecked ) { if ( elem[0] && typeof (len = elem.length) === "number" ) { for ( j = 0; j < len; j++ ) { findInputs( elem[j] ); } } else { findInputs( elem ); } } //合并转换后的DOM元素 if ( elem.nodeType ) {//如果是DOM元素直接push,elem本身就是DOM元素 ret.push( elem ); } else {//elem含有多个元素,合并 ret = jQuery.merge( ret, elem ); } } //提取script元素 if ( fragment ) { //检测是否是scriptType元素 rscriptType = /\/(java|ecma)script/i;rscriptType 标签script的type属性是否是javascript或ecmascrip checkScriptType = function( elem ) { return !elem.type || rscriptType.test( elem.type ); }; for ( i = 0; ret[i]; i++ ) { //如果ret[i]的标签名为script并且为指定type或type为text/javascript 放入scripts数组 if ( scripts && jQuery.nodeName( ret[i], "script" ) && (!ret[i].type || ret[i].type.toLowerCase() === "text/javascript") ) { scripts.push( ret[i].parentNode ? ret[i].parentNode.removeChild( ret[i] ) : ret[i] ); } else { /* 取出ret[i]下可能有的的script元素,调用jQuery.grep( elems, callback, inv )过滤,返回其中 未指定type或type包含/javascript或/ecmascript 的script元素数组jsTags,将这个数组插入ret[i]之后,下次循环时检查。 i + 1,将找到的jsTags通过splice插入ret[i],for循环下次遍历ret时,从jsTags开始遍历;splice()方法用于插入、删除或替换数组的元素 */ if ( ret[i].nodeType === 1 ) { var jsTags = jQuery.grep( ret[i].getElementsByTagName( "script" ), checkScriptType ); ret.splice.apply( ret, [i + 1, 0].concat( jsTags ) ); } fragment.appendChild( ret[i] ); } } } //返回转换后的DOM数组 return ret; },
相关文章推荐
- jquery源码分析——clean(elems,context,fragment,scripts)
- jQuery.buildFragment源码分析以及在构造jQuery对象的作用
- jQuery源码分析-12 DOM操作-Manipulation-核心函数jQuery.buildFragment()
- jQuery源码研究分析学习笔记-jQuery.buildFragment()(六)
- jQuery源码分析之buildFragment方法和clone方法
- [原创] jQuery源码分析-12 DOM操作-Manipulation-核心函数jQuery.buildFragment()
- jQuery源码分析系列:总体架构
- [原创] jQuery源码分析-12 DOM操作-Manipulation-核心函数.domManip()
- jQuery1.6.1源码分析系列
- jQuery源码分析系列
- [原创] jQuery1.6.1源码分析系列(持续更新)
- jQuery源码分析系列:Deferred延迟队列
- jQuery.buildFragment使用方法及思路分析
- jQuery源码分析系列:DOM遍历方法
- jQuery 2.0.3 源码分析系列
- jQuery源码分析系列:事件模块概述
- jQuery1.6.1源码分析系列
- jQuery1.6.1源码分析系列
- jQuery源码分析系列:Extend扩展方法
- jQuery源码分析系列:AJAX状态码