jQuery在删除或替换DOM节点前,会先执行内部方法cleanData。这个方法的主要任务不是把元素从页面上拿走,而是在节点真正脱离文档之前,解除挂在它身上的事件句柄、data缓存和expando属性。旧版IE(主要是IE6到IE8)使用引用计数而不是标记清除来回收对象,DOM节点与JavaScript对象之间如果存在循环引用,内存就会一直占用。SCRIPT元素在这类场景中更容易出问题,因为它一旦执行过代码,节点自身、脚本闭包、jQuery缓存三者之间可能形成多条引用链。jQuery在cleanData里对SCRIPT元素采用了不同于普通DIV的清理路径:先通过removeAttribute移除expando属性,再删除缓存,必要时还会清空脚本的src和事件回调。下面先看这段逻辑的底层背景。

旧版IE的引用计数与循环引用
IE6到IE8的垃圾回收机制以引用计数为主。对象每被引用一次,计数值加一;每解除一次引用,计数值减一。当计数值归零时,对象才会被回收。DOM节点在旧IE中属于COM对象,JavaScript函数是另一套对象,二者互相引用时,引用计数永远不会归零。比如给一个元素绑定事件处理函数,函数内部又保留了对这个元素的引用,就形成了一个典型的循环引用。
SCRIPT元素比普通容器更容易触发这种问题。动态创建脚本节点后,脚本内部可能注册了全局回调,或者闭包捕获了所在DOM分支。即使脚本节点从文档中被移除,闭包仍然通过作用域链持有SCRIPT节点,SCRIPT节点又通过属性或事件持有闭包,引用计数无法清零。jQuery的数据系统会往元素上添加expando属性,如果这个属性没有在节点移除前被正确剥离,缓存对象就会被DOM节点反向拖住,造成持续占用。
var div = document.createElement("div");
div.onclick = function() {
div.style.color = "red";
};
document.body.appendChild(div);
// 旧版IE下,即使执行document.body.removeChild(div)
// div依然被onclick闭包引用,引用计数不会归零
cleanData的职责与SCRIPT分支的实现
当对元素调用data方法或绑定事件时,jQuery会给元素添加一个类似jQuery123456789的自定义属性,这个属性的值是一个数字id,对应jQuery.cache对象中的某一项。绑定的事件句柄、自定义数据都存放在jQuery.cache[id]里。cleanData在元素删除前需要完成三件事:解绑事件、删除缓存项、移除expando属性。如果只删除DOM节点而不清理这些关联,jQuery.cache会不断膨胀,旧IE下的节点也可能继续引用缓存对象。
在expando属性清理这一步,现代浏览器可以安全使用delete操作符,但旧IE对动态添加属性的删除支持并不完整。jQuery为此做了特性检测:如果delete不可靠,就调用removeAttribute。对于SCRIPT元素,这一步更关键。SCRIPT节点在旧IE中被视为特殊节点,delete操作可能看起来成功,但属性引用仍然残留,甚至可能抛出错误。因此cleanData对SCRIPT节点走了removeAttribute分支,并且保证在删除缓存项之前先切断DOM到缓存的引用。
function cleanData(elems) {
var elem, i, id, data,
cache = jQuery.cache;
for (i = 0; (elem = elems[i]); i++) {
id = elem[jQuery.expando];
if (!id) continue;
data = cache[id];
if (data && data.events) {
for (var type in data.events) {
if (jQuery.event.special[type]) {
jQuery.event.remove(elem, type);
} else {
jQuery.removeEvent(elem, type, data.handle);
}
}
}
// SCRIPT节点在旧IE中必须走removeAttribute
if (elem.nodeName && elem.nodeName.toUpperCase() === "SCRIPT") {
elem.removeAttribute(jQuery.expando);
} else if (jQuery.support.deleteExpando) {
delete elem[jQuery.expando];
} else {
elem.removeAttribute(jQuery.expando);
}
delete cache[id];
}
}
这段简化实现展示了SCRipt元素分支的价值。先移除expando再删除缓存,是为了避免旧IE中出现DOM节点仍然通过expando持有旧id的情况。如果顺序颠倒,缓存项虽然被删除,但节点属性仍指向一个已经不存在的id,后续操作可能会读到错误数据,或者让垃圾回收器误判对象仍被引用。先切断DOM到缓存的路径,再让缓存对象失去引用,释放才会是确定的。
移除脚本节点前的附加清理
除了expando属性,SCRIPT元素自身的src、type、onload和onreadystatechange等属性也会在旧IE中加重内存负担。外部脚本的src被置空后,浏览器可以更早释放已缓存的脚本资源。onreadystatechange这类回调如果不显式断开,脚本节点移除后仍然可能被内部状态持有。jQuery在处理SCRIPT节点时,会在cleanData之外配合这些属性清理操作。
在旧版jQuery中,清理脚本节点通常不是简单调用removeChild。安全移除脚本节点的函数应当先调用jQuery.removeData触发cleanData,再断开脚本加载相关的回调,然后置空src,最后才把节点从DOM中拿掉。顺序很重要,因为回调一旦错过断开时机,节点移除后就会变成难以追踪的残留引用。
function safeRemoveScript(script) {
if (!script || script.nodeName !== "SCRIPT") {
return;
}
// 先清理jQuery数据,内部会调用cleanData
jQuery.removeData(script);
// 断开脚本加载相关回调
script.onload = null;
script.onerror = null;
script.onreadystatechange = null;
// 置空src让旧IE释放资源
if (script.src) {
script.src = "";
script.removeAttribute("src");
}
if (script.parentNode) {
script.parentNode.removeChild(script);
}
}
这个函数先把数据层清理干净,再处理脚本节点自身的浏览器级引用,最后执行DOM移除。对于使用jQuery 1.x并且需要兼容IE8的项目,这种顺序可以显著减少脚本节点残留。即使现代项目不再需要手动处理旧IE问题,理解这段顺序也有助于排查动态脚本加载后的内存占用异常。
现代浏览器是否还需要这些处理
现代浏览器的垃圾回收机制已经从引用计数切换为标记清除。标记清除不关心对象之间是否互相引用,只关心对象是否从根对象可达。因此,两个对象即使形成循环引用,只要它们不再被根对象引用,就仍然可以被回收。旧的DOM循环引用泄漏问题在IE11、Edge、Chrome、Firefox和Safari中已经基本消失。
jQuery从2.x版本开始不再支持IE6到IE8,也清理了大量针对旧IE的兼容代码。不过cleanData本身没有消失,因为解绑事件和清理data缓存对任何浏览器都是必要的。现代开发中如果使用jQuery 1.x维护旧系统,删除动态脚本时仍应优先使用jQuery的remove方法,而不是直接操作innerHTML或原生removeChild,否则jQuery.cache中的数据不会自动清理。
总结来看,jQuery在cleanData中对SCRIPT元素的特殊处理,核心思想是在垃圾回收器无法可靠工作时,通过显式解除DOM到缓存、脚本到回调之间的引用,把一个依赖GC的隐式问题转变成确定性的资源释放。这些看似冗余的removeAttribute和src置空操作,正是早期前端工程在旧IE环境下积累下来的防御性实践。

