在下这里有一个聊天框,然后实时插入聊天数据,过程大概如下:
// 向聊天框插入一条聊天信息. function appendMsg () { var newMsg = ""; $chatList.append(newMsg); // jQuery Function. }
然后我们限制聊天框最多填充一百条:
// 向聊天框插入一条聊天信息, 在消息大于 100 条后删除第一条. function appendMsg () { var newMsg = ""; $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 = ""; 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 p = document.createElement("p"); p.innerHTML = "papapa"; parent.children.length > 100 && parent.removeChild(parent.children[0]); parent.appendChild(p); }, 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 做的什么手脚没有深入查看(比如有缓存或其他行为),因此确实有可能是节点没有释放.
我会再抽时间进行查看,感谢 (・∀・)
每一条消息记录上,有绑定事件处理程序吗?
从文档中移除带有事件处理程序的元素时,原来添加到元素中的事件处理程序极有可能无法被当作垃圾回收。内存中留有那
些过时不用的“空事件处理程序”,是造成Web应用程序内存与性能问题的主要原因。
http://jquery.cuishifeng.cn/remove.html
jQuery会将每个dom都包装成一个jQuery对象,而jQuery的remove方法,只会删除dom节点,而保留jQuery包装对象,这应该是为了对象复用
楼主的例子1中:
$chatList.children().first().remove()
$chatList.append(newMsg)
这里删除后,jQuery对象保留,但后来append时是新创建的,所以会一直增加
楼主的例子2中:
$firstMsg.remove().html(newMsg)
这里直接使用了保留下来的jQuery对象来塞入新的子元素,所以对象得到了复用
我想,至少应该有这样一点需要注意到:
$chatList.children().length > 100 && $chatList.children().first().remove(); $chatList.append(newMsg);
这段代码把第1个节点取出来,从节点树中删除,然后产生了一个新节点对象加到节点树中,那么,这里创建了一个新的 Node 对象,这本身就比较花时间。而且垃圾收集机制会检查被删除的那个节点,如果它确实没被其它变量引用,会被回收。如果它仍然被其它变量引用(尤其需要注意闭包中的变量引用),这个节点还不会被回收,这种情况会造成资源一直被耗用却得不到回收。
if ($chatList.children().length > 100) { var $firstMsg = $chatList.children().first(); $firstMsg.remove().html(newMsg); $chatList.append($firstMsg); } else { $chatList.append(newMsg); }
而在这段代码中,如果已经产生了100个节点了,第一个节点对象被从节点树中移除,但对象本身会被复用,不会产生新的节点对象,也不会有节点对象被删除,节点资源保持在100个。