javascript - 关于 DOM 删除的性能疑惑

查看:162
本文介绍了javascript - 关于 DOM 删除的性能疑惑的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

问 题

在下这里有一个聊天框,然后实时插入聊天数据,过程大概如下:

// 向聊天框插入一条聊天信息.
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 做的什么手脚没有深入查看(比如有缓存或其他行为),因此确实有可能是节点没有释放.

我会再抽时间进行查看,感谢 (・∀・)

解决方案

我想,至少应该有这样一点需要注意到:

$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个。

这篇关于javascript - 关于 DOM 删除的性能疑惑的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆