在《[Jake]记一次前端内存泄漏事件(一):排查过程》中,我们定位到了内存泄漏时机是三个定时器触发时刻,内存泄漏点是名为observers的数组。
在《[Jake]记一次前端内存泄漏事件(二):解决方案》中,我们解决了observers数组膨胀的问题。
现在,我们需要找出触发变化检测的三个定时器。
原理
要在浏览器中设置定时器,无非就是用setTimeout或setInterval。
通过Performance工具,我们已经可以看到定时器触发时执行的事件处理函数。
在简单情形下,这已足矣帮我们找到设置定时器的代码。然而在本例中,我们将跳转到经过混淆的Angular框架代码中,且看不到setTimeout或setInterval的踪影。这是怎么回事?难道这个定时器是Angular自己设置的?当然不可能,否则性能多低呀。
我们知道,Angular会在每个异步事件发生时触发变化检测流程,setTimeout或setInterval定时器事件便是其中之一。这意味着异步事件触发时,Angular必须得到通知。谁来通知Angular呢?是NgZone这个服务。NgZone将代理所有异步事件的执行,并在合适的时候通知Angular。听起来是不是满满的黑科技的感觉。它是怎么做到的?更具体一点,它怎么知道setTimeout和setInterval的定时器触发了?大家思考一下,换你来,你怎么实现呢?
……
答案是……
……
……
它覆盖了全局的setTimeout和setInterval函数!!
是不是相当简单粗暴?也就是说,在Angular应用中,你在使用setTimeout和setInterval函数时,实际上调用的是NgZone所提供的一个包装函数,它内部包装了原生的setTimeout和setInterval函数,从而可以在真正的事件处理函数的执行前后附加各种钩子。
为了验证这一点,我们可以做个实验。首先打开google首页(非Angular应用),在控制台中输入setTimeout,然后点击输出结果中的函数名:
可以看到无法跳转,这说明setTimeout是由浏览器的原生代码实现的。
然后我们打开Angular官网(或其他Angular应用),同样在控制台中输入setTimeout,然后点击输出的函数名,可以发现跳转到了:
这说明setTimeout被覆盖了。
回到我们的问题,这也就是为什么我们点击Performance中的链接找不到真正的事件处理函数了。因为真正的事件处理函数被NgZone包装在其内部了,实际执行的事件处理函数是一个包装函数。
那么我们怎么找到真正的事件处理函数呢?很简单,我们自己也覆盖一次setTimeout和setInterval!具体点讲,在Angular覆盖之后,我们用自定义的setTimeout和setInterval覆盖原始版本,这样我们就可以其中记录真正的事件处理函数了。
实施
我们来看看NC 3.0启动时打开的页面。如果使用newkit官方开发包,其路径通常为node_modules/@newkit/development-package/dev-package/index.html。我们项目使用的是fork自官方开发包的自定义开发包,因此路径为node_modules/@ponk/development-package/dev-package/index.html。
打开该文件,可以看到启动时引用的各个脚本:
在angular-all.min.js引用之后,我们添加自定义函数覆盖setTimeout和setInterval:
然后重启应用,打开我们的Query PO页面,在控制台中打印timeouts和intervals这两个全局变量,来获取事件处理函数:
这里我们重点关注intervals,可以看到有4个定时器。展开后3个,得到函数脚本链接(暂时忽略第一个):
点击链接跳转到函数脚本:
找到了!这是nk-validator组件为了检测语言变化而设置的定时器。由于我们页面上有三个nk-validator,所以有三个定时器。频繁的触发定时器将导致严重的性能问题。这明显是个Issue,已向newkit提交。
为了验证这三个定时器的影响,我们通过clearInterval函数取消这三个定时器:(暂时忽略第一个)
再检测一次内存变化(100s):
注意上方红框,此时内存变化范围是76.0MB ~ 76.0 MB,说明内存再也没有变化了!(不要问我为什么图中折线还在上升,我也不知道……也许是有几个byte级别的增长吧)。
第一个定时器是什么?
最后一个问题,上面intervals打印出了四个定时器,后三个已证明是nk-validator,那第一个是什么?它为什么没有触发Angular变化检测?
我们跳转过去看一下:
代码:
这是newkit的layout组件设置的一个定时器,用于检查FunPanel的高度。请注意,这里没有直接使用setInterval,而是通过NgZone服务的runOutsideAngular方法来间接调用。关于该方法请参考官方文档:https://angular.io/api/core/NgZone#runoutsideangular。总之,该方法不会触发Angular的UI变化检测。因此如果有设置定时器的需求,又不想触发变化检测,那就请使用该方法。
总结
经过漫长的旅程,我们终于圆满解决了这个内存泄漏问题。在这个过程中,我们应该学习到:
1. Performance工具和Memory工具的使用。 1. 捕捉定时器的方法。 1. 不要在视图模板中调用会返回Observable的函数。 1. Angular中尽量不直接使用setTimeout和setInterval。 1. 使用runOutsideAngular来执行不想触发变化检测的异步任务。 |
|---|
