您的位置:首页 > Web前端 > JQuery

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内容。

可选值

功能

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元素)

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;
},
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: