为了避免涉密问题,文中提及的代码部分class名称被删除或打码,但不影响内容
前端时间发现办公电脑经常在下午因为内存不够卡死,然后发现是公司项目的页面吃的内存会随着使用有显著增加。由于这个项目是VUE的单页项目,不同页面的切换只是通过动态组件(component
)切换的,所以很长一段时间都不会刷新页面,因此这个问题会更加明显。
经过测试,在谷歌浏览器的开发人员工具中可以看出,在页面第一次加载时,内存占用甚至不到30M,但是反复重新加载同一页面后(将动态组件的is
属性设置为null
然后重新赋值)内存占用就会成倍增长,十几次之后,占用内存能超过200M。通过网上找到的排查方法可以看到有大量的游离(未挂载到页面显示)的dom元素。
难道是vue有问题不能正确处理?怎么想都觉得不可能,毕竟,如果是vue的问题,这个影响可太大了。那么会是哪里的问题呢?我在这里面看到了一个表单的dom元素,其实基本上也能确定问题所在了,不过为了确认这个问题,我还分块注释代码,然后逐步缩小范围,最后确定问题就是出在表单上。
这个表单呢,是这样做的基本上就是用的ElementUI的控件,但是呢,因为我们需要修改一些样式什么的,所以外面套了一层公司的控件,ElementUI作为槽插入其中。我这里能够确认到是外面这层控件的原因导致的了,接下来就是检查这个控件的活。
然后我们可以看到这里有一段用jquery做的部分。它的作用是把ElementUI原本显示在输入控件下方的信息变成鼠标悬停时显示(减少页面占用空间)。虽然好像是没啥问题,但是又总觉得不太对,于是我把这一段屏蔽了,然后问题顿时好了很多。
- /*
- * 创建验证悬浮窗
- */
- function createValidateToolTip($el) {
- //对form里面的所有el-input__inner添加鼠标进入和出去的事件,以在鼠标经过时(如果有的化)显示验证信息,出去时隐藏验证信息
- $($el)
- .find(".el-form-item__content")
- .each(function() {
- var $elinput = $(this).find(".el-input,.el-textarea__inner");
- var elFormItemContent = this;
- if ($elinput.size() > 0) {
- $($elinput).mouseenter(function() {
- var $findErrorItem = $(elFormItemContent).find(
- ".el-form-item__error"
- ); //看是否有错误验证提示信息
- if ($findErrorItem.size() > 0) {
- var validateMsg = $($findErrorItem.get(0)).html();
- //生成toolTip
- //生成外围div
- var $toolTip = $(
- '<div class=""></div>'
- ).appendTo($("body"));
- $(this).data("toolTip", $toolTip); //存放到元素的data里面,方便后面找到他然后移除
- $toolTip.width(strlen(validateMsg) * 6);
- //生成内容div
- var $toolTipContent = $(
- '<div class=""></div>'
- ).appendTo($toolTip);
- $toolTipContent.html(validateMsg);
- //生成箭头外围div
- var $toolTipArrowOuter = $(
- '<div class="" ></div>'
- ).appendTo($toolTip);
- //生成箭头div
- var $toolTipArrow = $(
- '<div class="" ></div>'
- ).appendTo($toolTip);
- var offset = $(this).offset();
- var toolTipOffset = {
- top:
- offset.top +
- ($(this).outerHeight() - $toolTip.outerHeight()) / 2,
- left: offset.left + $(this).outerWidth() + 8
- };
- $toolTip.offset(toolTipOffset);
- }
- });
- $($elinput).mouseleave(function() {
- if ($(this).data("toolTip"))
- $(this)
- .data("toolTip")
- .remove();
- });
- }
- });
- }
可是,为什么这里会出现这个问题呢?首先我想到以前说过jQuery是会做一些缓存提高性能的,可是怎么缓存的呢?抱着试一下的心态随便在控制台中输入$.
然后看到一个cache
字段的提示,赶紧打出来一看,果然有东西!重新加载页面之后,他还正好增加了一倍。
这样一来,问题就算是找到了,vue销毁了页面,但是jQuery并不知道,并且这些元素会存放在全局的缓存中(即$.cache
),然后这些dom因为仍然存在引用,所以不能被释放掉。
那么接下来就是解决这个问题了。其实吧我一开始以为是jquery选择器的缓存,可是查阅资料得知选择器不会缓存,差点没了头绪,好在点开一看,是事件的缓存。并且在这个事件的缓存中通过Scopes
字段引用的dom,然后dom又有children
和parentNode
这一类的字段,相互引用着,导致整个页面都还处于引用状态。那么接下来就是怎么解决这个问题了。其实也挺简单的,在控件销毁前(beforeDestroy
钩子)把这些事件解注册就行了:
- $(this.$el).find(".el-input,.el-textarea__inner").off();
然后再看页面,反复加载$.cache
也没有增长:
相应的,页面游离的dom也没有增加了(不过还是有闭包之类的在增加,不过那些暂时就不查了,至少现在内存占用已经控制住了):
OK,准备收工。可是忽然有发现出现了没被释放的dom!难道?点开看看是啥:
可以看到这是缓存了data
属性,不知道大家还记得之前确实有这么一句东西
- $(this).data("toolTip", $toolTip);
对就是它同样需要手动释放掉,所以得到最终资源释放的代码如下:
- $(this.$el).find(".el-input,.el-textarea__inner").removeData("toolTip").off();
OK,虽然现在页面内存泄漏的问题仍然存在,但是速度已经控制住了,并且考虑运行我们产品的客户端一般不会打开其他网页和软件,只是可能很长时间(甚至几个月)都会一直开着,所以这样基本上还是可以接受了吧。