乐趣区

关于前端:内存泄漏以及eslint内存泄漏排查工具开发

内存透露以及 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-pkgs
      pnpm link ../eslint-plugin-memory-leak
      npx 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,3
      N(1) -> 0,2
      N(2) -> 0,1
      N(3) -> 0,4
      N(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 内存透露排查插件只实现了很小一部分,相干解决也有很多有待晋升的中央,后续随着相干内容的学习和开发将进一步欠缺。
退出移动版