内存透露以及 eslint 内存透露排查工具开发
次要内容有两局部:
- 内存透露的起因及场景案例
- 依据案例开发 eslint 内存透露排查插件
前端内存透露问题
内存透露起因
像 js 这种自带垃圾回收机制的语言,产生内存透露的起因不外乎是定义了全局的对象、事件,定时器,而没有及时清理,导致随着程序运行,内存占用越来越高。
全局事件
常见于在 window、document 上增加事件未及时解绑。须要留神的是,绑定事件必须保障 addEvenetListener
和removeEventListener
参数的一致性
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()
}, [])
}
定时器
诸如 setInterval
、requestAnimationFrame
这类生成全局定时器的函数,须要在退出对应业务代码生命周期时进行销毁。
惯例状况比拟容易判断,但异步递归 + 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 树中,节点都蕴含start
、end
参数,可用来断定代码块之间的关系;标记完起止值后,仅需 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 内存透露排查插件只实现了很小一部分,相干解决也有很多有待晋升的中央,后续随着相干内容的学习和开发将进一步欠缺。