[原创] jQuery源码分析-12 DOM操作-Manipulation-核心函数.domManip()
作者:nuysoft/高云 QQ:47214707 Email:nuysoft@gmail.com
声明:本文为原创文章,如需转载,请注明来源并保留原文链接。
前记:
基于 jQuery 1.7.1 编写;之前的系列文章以“贴源码注释”的方式进行讲解,注释并不适合做大段的描述和排版;本节将尝试 锚点+按块分析+流程图 的方式,希望这样能增加更详细的描述,方便阅读理解和加深记忆。
核心函数 .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执行实际的插入操作
关于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() |
.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元素