在下这里有一个聊天框,然后实时插入聊天数据,过程大概如下:
// 向聊天框插入一条聊天信息.
function appendMsg () {
var newMsg = "<some-html-string></some-html-string>";
$chatList.append(newMsg); // jQuery Function.
}
然后我们限制聊天框最多填充一百条:
// 向聊天框插入一条聊天信息, 在消息大于 100 条后删除第一条.
function appendMsg () {
var newMsg = "<some-html-string></some-html-string>";
$chatList.children().length > 100 && $chatList.children().first().remove();
$chatList.append(newMsg);
}
看起来好像还 OK,不过当用户多起来(20000+)、聊天区疯狂刷起的情况下,浏览器性能会出现明显下降,使用 Chrome 开发者工具进行分析,Timeline
中的 Nodes
数呈直线上升,但总内存使用量依旧保持在一个固定范围,JS Heap
的悬崖形态的图标也表示 GC 过程正常.
此时有一个疑问:$(...).remove() / removeChild() 在移除节点后为什么开发者工具中的 Nodes 依然呈上升并造成浏览器性能明显下降,但内存依然被正常回收?
在尝试多个优化后效果改善不大,后来尝试更改节点限制的操作逻辑:超过 100 时将第一个聊天项节点取出,修改 HTML 后再放回聊天列表
,代码大概如下:
// 向聊天框插入一条聊天信息, 在消息大于 100 条后删除第一条.
function appendMsg () {
var newMsg = "<some-html-string></some-html-string>";
if ($chatList.children().length > 100) {
var $firstMsg = $chatList.children().first();
$firstMsg.remove().html(newMsg);
$chatList.append($firstMsg);
} else {
$chatList.append(newMsg);
}
}
使用如上策略后,性能问题消失,且 Chrome 开发者工具中 Timeline 里面的 Nodes
曲线表现和 JS Heap
相同,即在一段时间后会被回收,然后再次上涨,之后再次回收。
在下深感迷惑,完全不同的结果,难道是因为后者 append
的节点时取自页面,而非新的变量?是因为前者内存没有回收干净?但开发者工具表示内存已经得到回收;是因为后者的 Nodes
得到正确回收?但两者不都是普通的 remove
操作么,为何前者疯涨?这个 Nodes
到底代表的是什么?但是遗憾的是,在爆栈上搜索了很多内容都没有找到与 “Nodes、Dom 移除后的 GC 行为” 相关的明确内容,大部分都比较含糊,也许是因为姿势不对吧 (´;ω;`)
另外关于这个 Nodes
:
var parent = document.getElementById("parent");
setInterval(function () {
var div = document.createElement("div");
div.innerHTML = "papapa";
parent.children.length > 100 && parent.removeChild(parent.children[0]);
parent.appendChild(div);
}, 10);
一个有这样一个简单计时器的页面,整个页面的节点应该在 100+,不过开发者工具中的 Nodes
数量一直保持在 200+ 的水平,所以这个 Nodes 到底是什么
(´・_・`)
写的比较混乱,如果有哪位能指点一二,在下感激不尽!(・∀・)
Update:
受到两位指点,简单看了一下 jQuery 中关于 remove 的代码部分,可能理解不正确,还请多指教。
remove 部分代码大概如下:
jQuery.fn.extend({
//...
remove: function( selector ) {
return remove( this, selector );
}
});
// Remove 函数.
function remove( elem, selector, keepData ) {
var node,
nodes = selector ? jQuery.filter( selector, elem ) : elem,
i = 0;
for ( ; ( node = nodes[ i ] ) != null; i++ ) {
if ( !keepData && node.nodeType === 1 ) {
jQuery.cleanData( getAll( node ) ); // 第一步:清除节点信息.
}
if ( node.parentNode ) {
if ( keepData && jQuery.contains( node.ownerDocument, node ) ) {
setGlobalEval( getAll( node, "script" ) ); // 没有具体查看是干嘛的.
}
node.parentNode.removeChild( node ); // 第二步:调用 removeChild 删除节点.
}
}
return elem; // 最后返回清理后的节点.
}
// clearData 函数.
cleanData: function( elems ) {
var data, elem, type,
special = jQuery.event.special,
i = 0;
for ( ; ( elem = elems[ i ] ) !== undefined; i++ ) {
if ( acceptData( elem ) ) {
if ( ( data = elem[ dataPriv.expando ] ) ) {
if ( data.events ) {
for ( type in data.events ) {
if ( special[ type ] ) {
jQuery.event.remove( elem, type );
// This is a shortcut to avoid jQuery.event.remove's overhead
} else {
jQuery.removeEvent( elem, type, data.handle );
}
}
}
// Support: Chrome <= 35-45+
// Assign undefined instead of using delete, see Data#remove
elem[ dataPriv.expando ] = undefined;
}
if ( elem[ dataUser.expando ] ) {
// Support: Chrome <= 35-45+
// Assign undefined instead of using delete, see Data#remove
elem[ dataUser.expando ] = undefined;
}
}
}
}
因此看起来确实在调用 remove()
后仅仅移除了 Dom 节点和清除 Dom 信息,至于在其他阶段 jQuery 做的什么手脚没有深入查看(比如有缓存或其他行为),因此确实有可能是节点没有释放.
我会再抽时间进行查看,感谢 (・∀・)
我想,至少应该有这样一点需要注意到:
这段代码把第1个节点取出来,从节点树中删除,然后产生了一个新节点对象加到节点树中,那么,这里创建了一个新的 Node 对象,这本身就比较花时间。而且垃圾收集机制会检查被删除的那个节点,如果它确实没被其它变量引用,会被回收。如果它仍然被其它变量引用(尤其需要注意闭包中的变量引用),这个节点还不会被回收,这种情况会造成资源一直被耗用却得不到回收。
而在这段代码中,如果已经产生了100个节点了,第一个节点对象被从节点树中移除,但对象本身会被复用,不会产生新的节点对象,也不会有节点对象被删除,节点资源保持在100个。