jQuery源码分析之empty和remove方法四问

时间:2021-01-06 14:26:31

注意:必须阅读,弄懂什么是仓库,什么是钥匙

测试代码1:把id为first下面所有的子元素移除

                        <html>
<head>
<script type="text/javascript" src="/jquery/jquery.js"></script>
<script type="text/javascript">
$(document).ready(function(){
var ps=document.getElementById("first");
while(ps.firstChild)
{
ps.removeChild(ps.firstChild);
}
});
</script>
</head>
<body>
<p id="first">
<span>This is a paragraph.</span>
<span>This is a paragraph.</span>
</p>
</body>
</html>
empty源码解析:

empty: function() {
var elem,
i = 0;
for ( ; (elem = this[i]) != null; i++ ) {
// Remove element nodes and prevent memory leaks
if ( elem.nodeType === 1 ) {
//传入调用对象的DOM元素,同时传入false作为第二个参数!查找该DOM元素下的所有的元素节点,返回所有的节点
//同时把该所有的节点通过$.data方法保存的数据清除!这样可以防止元素被移除了,但是该元素还保存数据!内存泄漏!
jQuery.cleanData( getAll( elem, false ) );
}
// Remove any remaining nodes
//移除节点
while ( elem.firstChild ) {
elem.removeChild( elem.firstChild );
}
//在IE<9时候对select标签做特殊处理!
// If this is a select, ensure that it displays empty (#12336)
// Support: IE<9
if ( elem.options && jQuery.nodeName( elem, "select" ) ) {
elem.options.length = 0;
}
}
return this;
}

getAll方法:

function getAll( context, tag ) {
var elems, elem,
i = 0,
found = typeof context.getElementsByTagName !== strundefined ? context.getElementsByTagName( tag || "*" ) :
typeof context.querySelectorAll !== strundefined ? context.querySelectorAll( tag || "*" ) :
undefined;
//如果getElementsByTagName和 querySelectorAll 都不存在那么返回undefined,通过jQuery.nodeName来选择结果
if ( !found ) {
for ( found = [], elems = context.childNodes || context; (elem = elems[i]) != null; i++ ) {
if ( !tag || jQuery.nodeName( elem, tag ) ) {
found.push( elem );
} else {
jQuery.merge( found, getAll( elem, tag ) );
}
}
}

return tag === undefined || tag && jQuery.nodeName( context, tag ) ?
jQuery.merge( [ context ], found ) :
found;
}

internalData函数点击打开链接

function internalData( elem, name, data, pvt /* Internal Use Only */ ) {
if ( !jQuery.acceptData( elem ) ) {
return;
}
var ret, thisCache,
internalKey = jQuery.expando,
// We have to handle DOM nodes and JS objects differently because IE6-7
// can't GC object references properly across the DOM-JS boundary
isNode = elem.nodeType,
// Only DOM nodes need the global jQuery cache; JS object data is
// attached directly to the object so GC can occur automatically
cache = isNode ? jQuery.cache : elem,
// Only defining an ID for JS objects if its cache already exists allows
// the code to shortcut on the same path as a DOM node with no cache
id = isNode ? elem[ internalKey ] : elem[ internalKey ] && internalKey;

// Avoid doing any more work than we need to when trying to get data on an
// object that has no data at all
if ( (!id || !cache[id] || (!pvt && !cache[id].data)) && data === undefined && typeof name === "string" ) {
return;
}
if ( !id ) {
// Only DOM nodes need a new unique ID for each element since their data
// ends up in the global cache
if ( isNode ) {
id = elem[ internalKey ] = deletedIds.pop() || jQuery.guid++;
} else {
id = internalKey;
}
}
if ( !cache[ id ] ) {
// Avoid exposing jQuery metadata on plain JS objects when the object
// is serialized using JSON.stringify
cache[ id ] = isNode ? {} : { toJSON: jQuery.noop };
}
// An object can be passed to jQuery.data instead of a key/value pair; this gets
// shallow copied over onto the existing cache
if ( typeof name === "object" || typeof name === "function" ) {
if ( pvt ) {
cache[ id ] = jQuery.extend( cache[ id ], name );
} else {
cache[ id ].data = jQuery.extend( cache[ id ].data, name );
}
}
thisCache = cache[ id ];
// jQuery data() is stored in a separate object inside the object's internal data
// cache in order to avoid key collisions between internal data and user-defined
// data.
if ( !pvt ) {
if ( !thisCache.data ) {
thisCache.data = {};
}

thisCache = thisCache.data;
}

if ( data !== undefined ) {
thisCache[ jQuery.camelCase( name ) ] = data;
}
// Check for both converted-to-camel and non-converted data property names
// If a data property was specified
if ( typeof name === "string" ) {

// First Try to find as-is property data
ret = thisCache[ name ];

// Test for null|undefined property data
if ( ret == null ) {

// Try to find the camelCased property
ret = thisCache[ jQuery.camelCase( name ) ];
}
} else {
ret = thisCache;
}

return ret;
}
setGlobalEval函数:

function setGlobalEval( elems, refElements ) {
var elem,
i = 0;
for ( ; (elem = elems[i]) != null; i++ ) {
jQuery._data( elem, "globalEval", !refElements || jQuery._data( refElements[i], "globalEval" ) );
}
}
jQuery._data函数:

_data: function( elem, name, data ) {//传入了参数为:script元素对象,"globalEval",true参见<a target=_blank href="http://blog.csdn.net/liangklfang/article/details/48781809">点击打开链接</a>
return internalData( elem, name, data, true );
}
remove源码:

remove: function( selector, keepData /* Internal Use Only */ ) {
var elem,
//如果传入selector,那么把调用对象通过selector进行删选,否则不筛选!
elems = selector ? jQuery.filter( selector, this ) : this,
i = 0;
//对调用对象进行逐个处理
for ( ; (elem = elems[i]) != null; i++ ) {
//如果没有传入第二个参数,同时当前调用对象的元素是Element那么连同数据一起清除!
//默认清除数据!
if ( !keepData && elem.nodeType === 1 ) {
jQuery.cleanData( getAll( elem ) );
}
//如果该元素有parentNode
if ( elem.parentNode ) {
//如果有parentNode的同时要保存数据,而且该调用对象元素在当前文档中
if ( keepData && jQuery.contains( elem.ownerDocument, elem ) ) {
//获取该Element元素下面的所有的script标签,得到一个NodeList对象
setGlobalEval( getAll( elem, "script" ) );
}
//移除该元素
elem.parentNode.removeChild( elem );
}
}

return this;
}

问题1:remove默认会移除自身+子元素下面的所有的事件和数据。调用方式remove()

$("#p").data("name","qinliang");
$("#p").click(function()
{
console.log("click");
});
var expando=jQuery.expando;//准备获取钥匙
var key=$("#p")[0][expando];//获取钥匙
var walhouse=jQuery.cache;//获取仓库
var data=walhouse[key];//用钥匙打开仓库
console.log(data);//数据存在
var jquery=$("div").remove();//移除了元素div,同时默认的情况下div+div子元素保存的数据和事件都移除了
jquery.appendTo($("body"));
var data=$("#p").data("name");//打印undefined,默认的事件click也不能触发了!
console.log(data);

note:最后打印undefined,因为数据都被移除了,包括事件和数据!
我们可以不让他移除数据,那么当我重新添加到DOM树中,数据仍存在,事件也能触发调用方式remove(undefined,true);

$("#p").data("name","qinliang");
$("#p").click(function()
{
console.log("click");
});
var expando=jQuery.expando;//准备获取钥匙
var key=$("#p")[0][expando];//获取钥匙
var walhouse=jQuery.cache;//获取仓库
var data=walhouse[key];//用钥匙打开仓库
console.log(data);//仓库里面有数据
var result=$("#div").remove(undefined,true);
//移除元素,但是保存元素和子元素的所有的数据!同时虽然从DOM中移除了,但是还在jQuery中保存了引用
result.appendTo($("body"));
//把jQuery中的数据重新添加到DOM树中!
var data1=$("#p").data("name");
//仍然能够获取到数据,也就是说上面的调用方式使得我们保存了数据!打印qinliang,点击p元素仍然可以触发click事件
console.log(data1);

问题2:如果移除的元素下面有script标签怎么办?

看下面的代码:

 setGlobalEval( getAll( elem, "script" ) );  
那么具体表现是什么呢

 var div=$("#div").remove(undefined,true);
//移除script标签
div.appendTo($("body"));
//重现添加script到DOM中
var expando=jQuery.expando;//准备获取钥匙
var key=$("#me")[0][expando];//获取钥匙
var walhouse=jQuery.cache;//获取仓库
var data=walhouse[key];//用钥匙打开仓库
console.log(data);//仓库里面有数据,globalEval有参数

解答:从这里例子,你可以看出来当移除div元素后重新添加了,那么这个script已经有标记为globalEval表示已经执行过了!见该图,HTML部分见下:

<div id="div" style="width:100px;height:100px;background-color:red;">
<script id="me">
console.log(123);
</script>
<p style="width:100px;height:50px;background-color:#ccc;" id="p">I am p</p>
</div>

问题3:empty方法和remove方法有什么差别?

解答:就移除DOM来说,empty移除所有子元素,而remove连自己也移除;就移除数据来说,empty只会移除子元素的数据,而remove会移除包括自身的数据!

是否移除数据解答:

remove部分:

$("#div").data("name","qinliang");
$("#div").remove();
var result=$("#div").data("name");
console.log(result);//remove是undefined
note:调用remove后数据也移除了!
empty部分:

$("#div").data("name","qinliang");
$("#div").empty();
var result=$("#div").data("name");
console.log(result);//remove是qinliang
note:empty后数据还是存在的!
是否移除DOM的解答:

$("#div").data("name","sex");
$("#div").empty();
alert($("#div")[0].id);//打印div

note:调用该元素的empty时候,那么元素本身没有移除,只是移除了所有的子元素和子元素上面的数据,所以访问id还是能访问到!如果把上面的empty修改为remove,那么该元素已经被移除了,访问他的id就会报错,至于他上面在移除前保存的数据也会消失,之所以会消失是因为在remove里面调用代码是getAll(elem);所以在getAll方法里面tag===undefined,所以会返回包括本身也就是调用对象自己的一个数组集合!因此结果就是把该元素以及该元素下所有的子元素的数据和事件全部移除(所以当后面append到body的时候也是没有数据和事件的)。

问题4:detach和remove有什么区别?

detach相当于调用remove时候传入了第二个参数是true,所以他不会移除数据和事件,所以当下次把元素重新append调用DOM树上时候,数据和事件还是存在的,而remove就会消失;如果删除的DOM中含有script元素的子标签,那么会执行script,同时把该script标签的globalEval设置为true,但是记住,只有调用detach方法才会设置该script元素的globalEval为true!

对于detach来说,调用的时候第二个参数是true所以不会移除该元素和该元素子元素下的所有的数据!见下例:

   HTML部分:

<div id="google">Google</div>
<div id="apple">
<div id="huawei">
我是华为
</div>
</div>
JS部分:

 $("#apple").hover(function () {
$(this).text("Google+");
});
$("#apple").data("sex","female");
$("#huawei").data("sex","male");
apple = $("#apple").detach();//移除apple节点
$("body").append(apple);//重新把apple节点添加到DOM树!
console.log($("#apple").data("sex"));//打印female
console.log($("#huawei").data("sex"));//打印male

note:调用detach元素的DOM,自身和子元素的数据都会保存,同时事件也会保存!

remove方法数据和事件都会移除

<div id="google">Google</div>
<div id="apple" style="background-color:red">
<div id="huawei">
我是华为
</div>
</div>
 JS部分:

 $("#apple").hover(function () {
$(this).text("Google+");
});
$("#apple").data("sex","female");
$("#huawei").data("sex","male");
apple = $("#apple").remove();//移除apple节点
$("body").append(apple);//重新把apple节点添加到DOM树!
console.log($("#apple").data("sex"));//打印undefined
console.log($("#huawei").data("sex"));//打印undefined
note:调用remove方法,那么数据和事件都会被移除,下次添加到DOM后也消失了!
detach的本质就是remove方法的第二个参数是true!

HTML部分:

<div id="google">Google</div>
<div id="apple" style="background-color:red">
<div id="huawei">
我是华为
</div>
</div>
JS部分:

$("#apple").hover(function () {
$(this).text("Google+");
});
$("#apple").data("sex","female");
$("#huawei").data("sex","male");
apple = $("#apple").remove(undefined,true);//移除apple节点
$("body").append(apple);//重新把apple节点添加到DOM树!
console.log($("#apple").data("sex"));//打印female
console.log($("#huawei").data("sex"));//打印male
note:这种调用就是detach方法,他会保存数据和事件,包括自己的和子元素的!

总结:

(1)empty方法会把该元素下面的所有子元素移除,但是移除之前会把这个元素的子元素通过$.data保存的数据和事件都移除掉(调用cleanData方法)!进而防止内存泄漏,同时对IE<9处理select做了兼容,用的是elem.options.length=0

(2)remove方法通过传入的参数对象对调用对象进行筛选,调用的是cleanData方法,该方法会移除该元素以及该元素的子元素所有的数据和事件处理器。内部调用传入第二个参数可以用来保存数据,在detach方法源码中有体现。

(3)要注意上面的empry方法调用getAll(elem,false)所以返回的元素不包括当前调用对象,但是在remove方法中调用的是getAll(elem)所以会包括调用对象