内存透露以及eslint内存透露排查工具开发

次要内容有两局部:

  1. 内存透露的起因及场景案例
  2. 依据案例开发eslint内存透露排查插件

前端内存透露问题

内存透露起因

像js这种自带垃圾回收机制的语言,产生内存透露的起因不外乎是定义了全局的对象、事件,定时器,而没有及时清理,导致随着程序运行,内存占用越来越高。

全局事件

常见于在window、document上增加事件未及时解绑。须要留神的是,绑定事件必须保障addEvenetListenerremoveEventListener参数的一致性

const f = function(){}// eq1 解绑失败,f.bind(this)生成了新的函数对象window.addEvenetListener('click', f.bind(this))window.removeEvenetListener('click', f.bind(this))// eq2 解绑失败,第三个参数不统一window.addEvenetListener('click', f, false)window.removeEvenetListener('click', f, true)// 查看window绑定函数getEventListeners(window)

全局变量

全局变量不肯定是指挂在window下的对象,单个js文件中,间接定义该文件在文件全局下的变量也属于全局变量。当某个代码逻辑生命周期中批改了全局变量后,该生命周期完结时,应移除批改局部。

import 'echarts' from echarts;const obj = {};// 比拟危险办法,批改了全局变量obj,且作为导出项,不可控export const injectData = (key, val) => {  obj[key] = val;}......// 应用第三方库时,应留神文档中对于创建对象的形容// echarts.init生成chart对象时,会将该对象挂载在全局的echarts下,调用dispose办法能力销毁function Comp() {  useEffect(()=>{      const chart = echarts.init(element, undefined, opts);          chart.setOption(option);        // 此处应加上正文中的代码        // return () => chart.dispose()  }, [])}

定时器

诸如setIntervalrequestAnimationFrame这类生成全局定时器的函数,须要在退出对应业务代码生命周期时进行销毁。

惯例状况比拟容易判断,但异步递归 + setTimeout的场景容易被疏忽:

let timer;async function loopFetchData() {  const data = await fetchData();    timer = setTimeout(()=>{    loopFetchData();  }, 3000)}......// 退出生命周期时clearTimeout(timer)timer = null

如上所示用例,看似在退出生命周期时销毁了定时器,但认真看便能发现,该操作疏忽了fetchData处于申请中的状态。

该代码状态如下图所示

若要正确完结递归调用,需完结所有虚线示意的异步代码。

较为正当的形式是定义一个退出循环的判断条件,代码如下所示

let toStop;async function loopFetchData() {  const data = await fetchData();  if(toStop) return;    timer = setTimeout(()=>{    loopFetchData();  }, 3000)}......// 进入生命周期时toStop = false;......// 退出生命周期时toStop = true;

如此,代码构造如下,整个过程有了终结态

eslint内存透露排查插件

后面列举了常见的前端内存透露情景,是否能够开发一个eslint插件,辅助内存透露的排查工作?

最近开发进度比拟紧,没来得及实现所有内存透露场景的插件,只开发了递归调用合法性断定插件。

eslint插件开发及原理

这部分内容网上比拟多,参考文章

递归调用合法性断定插件

后面说了定时器未回收的问题,咱们能够发现,递归调用无论同步异步,最好还是蕴含return,以防止不必要的问题,因而开发了相干插件。地址如下:

  • 代码地址
  • 对应rules地址

测试形式:

  • 全局装置pnpm
  • 进入代码仓库,pnpm install后执行如下操作

    • cd ./test-pkgspnpm link ../eslint-plugin-memory-leaknpx eslint ./
    • 可在控制台中查看test-pkgs/index.js文件的错误信息

      递归的断定

      函数之间的关系断定

      假如函数的援用关系如下,如何最快断定任意两个节点之间的关系(蕴含、a在b之前等)?

      咱们能够在dfs的同时,给各个节点加上进入、退出的count。以节点a为例,进入时标记起始值为count,再进入子节点d,给d标记起始值count+1,退出d,给d标记终止值count+2,再退出a,a标记终止值count+3......

      能够看到,子节点的起止值必然是父节点的起止值的子集,a节点在b节点之前,则必然满足a.end > b.start。大部分库生成的ast树中,节点都蕴含startend参数,可用来断定代码块之间的关系;标记完起止值后,仅需O(1)的工夫就能获取节点之间的关系。

      函数援用的有向图是否造成环

      假如函数都是一个节点,方向指的是函数的应用状况,例如:

      function a() {b()c()}

      则有向图的方向示意为a->[b,c]

      能够发现,函数的应用逻辑整体是一个有向图,而咱们须要做的是断定有向图中是否存在环。

      值得注意的是,此处有向图是否蕴含环的断定,和常见相似算法题不一样的是,并不是要在生成整个有向图之后进行断定,而是在生成有向图的过程中,每增加一个节点都要断定一次

      参考无向图找环的Union Find办法:

      N(0) -> 1,2,3N(1) -> 0,2N(2) -> 0,1N(3) -> 0,4N(4) -> 3

      因为 N(2)∈(N(0) ∪ N(1)),能够断定节点0、1、2组成环。

      将此逻辑利用到有向图上

      函数之间的应用关系如下, 能够采纳dfs+banList获取各个函数的应用项汇合,断定函数的子集中是否蕴含本身:

      N(0) -> 2,3,N(2),N(3)N(1) -> 0,N(0)N(2) -> 1,N(1)N(3) -> 4,N(4)
注:思否这里md语法如同有问题,改了半天没改好。。### 小结因为开发工夫有余,以及程度无限,eslint内存透露排查插件只实现了很小一部分,相干解决也有很多有待晋升的中央,后续随着相干内容的学习和开发将进一步欠缺。