【方案篇】事件监听函数的内存泄漏,帮你搞定!

本文是 理论篇 ,还有下篇 代码篇

前言

工作中,我们会对window, DOM节点,WebSoket, 或者单纯的事件中心等注册事件监听函数。

// window
window.addEventListener("message", this.onMessage);
// WebSoket
socket.addEventListener('message', function (event) {console.log('Message from server ', event.data);
});
// EventEmitter
emitter.on("user-logined", this.onUserLogined);

要是没有移除,就可能导致内存的泄漏

SPA更加剧了这种现象
比如React组件加载后,在window上注册了监听事件,组件卸载没有删除,极有可能像滚雪球一样,一发不可收拾。

componentDidMount(){window.addEventListener("resize", this.onResize);
}
componentWillUnmount(){// 忘记remove EventListener
}

今天我们的主题,就是分析事件监听,并排查因此可能导致的内存泄漏。

本文主要讨论和分析几个技术点:

  1. 怎么准确知道某个对象或者函数是否被回收
  2. 常见事件监听函数的本质
  3. 如何收集DOM事件监听函数
  4. 拦截方法常见方式
  5. 弱引用回收问题
  6. 如何裁定事件监听函数重复

效果演示

报警高危事件统计事件统计等功能,我们一起来看看效果吧。

预警

当你进行事件注册的时候,如果发现四同属性的事件监听,就进行报警。
四同:

  1. 同一事件从属对象
    比如Window, Socket等同一个实例
  2. 事件类型,比如 message, resizeplay等等
  3. 事件回调函数
  4. 事件回调函数选项

截图来自我对实际项目的分析, message事件重复添加,预警!!

image.png

高危统计

高危统计是对预警的拔高,他会统计 四同属性 的事件监听。是排查事件回调函数泄漏最重要方法。

DOM事件

截图来自我对实际项目的分析 , window对象上message消息的重复添加, 次数高达10
image.png

EventEmitter模块
截图来自我对实际项目的分析 ,APP_ACT_COM_HIDE_ 系列事件重复添加
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

事件统计

按照类型,罗列所有的事件监听回调函数,包含函数名,也可以包含函数体。
这中我们分析问题也是极其有用的。
image.png

所以,

各位大哥,各位大哥,各位大哥, 务必给函数起个名字,务必给函数起个名字,务必给函数起个名字。

这时候,名字真的很重要!!!!!!!

怎么准确知道某个对象或者函数是否被回收

一种直观有效的答案: 弱引用 WeakRef + GC(垃圾回收)

为什么要弱引用呢? 因为我们不能因为我们的分析和统计影响对象的回收?不然分析肯定也不准了。

弱引用

WeakRef 是ES2021提出来的,用于直接创建对象的弱引用, 不会妨碍原始对象被垃圾回收机制清除。

WeakRef 实例对象有一个deref()方法,如果原始对象存在,该方法返回原始对象;如果原始对象已经被垃圾回收机制清除,该方法返回undefined

let target = {};
let wr = new WeakRef(target);let obj = wr.deref();
if (obj) { // target 未被垃圾回收机制清除// ...
}

来看一个实际的例子:
左边target不会被回收,右边会被回收。
image.png

看到这里,你应该至少有两个意识:

  1. window.gc()是什么鬼
    其是v8提供的方法,主动触发垃圾回收,接下来会提到。
  2. IIFE这种闭包的应用,确实可以程度减少变量污染和泄漏

垃圾回收

垃圾回收是有周期的,以chrome浏览器为例,是可以主动执行垃圾回收的。 本应该被回收的对象,主动执行回收操作之后,倘若他还在,那很大可能性就导致了泄漏。

基于v8引擎的浏览器,怎么主动执行垃圾回收呢?
答案是: 修改chrome的启动参数,加上 --js-flags="--expose-gc"即可

image.png

之后,你就可以只直接调用gc方法进行垃圾回收

image.png

小结

有了 WeakRef + 主动GC
你就可以在你觉得可能泄漏或污染的地方进行测试,排查问题。

通过现象看本质

事件监听的表象

回归主题,我们今天的重点是事件回调函数,
我们的在web编程中常遇见的事件订阅类型表象有:

  • DOM事件
    主要是DOM2级别的事件,也就是addEventListener, removeEventListener
  • WebSocket, socket.io, ws, mqtt等等这

本质上来看,也是两种:

  • 基于 EventTarget 的事件订阅
    window, document , body, div等等这种常见DOM相关的元素,XMLHttpRequest, WebSocketAudioContext 等等,其本质都是继承了EventTarget。
  • 基于 EventEmitter的事件订阅
    mqttws 是基于 events 。
    著名的 socket.io是基于component-emitter。
    其都有共同点就是通过 onoff等方法来订阅和取消事件。

所以,我们要想监听和收集事件的订阅和取消订阅的信息,就变得简单了。

本质 - prototype

不管是EventTarget系列还是EventEmitter系列, 其本质都是实例化之后,在实例上进行订阅和取消订阅。

而实例化,为了更加好复用和更少的内存开销,一般会把公用的方法放到prototype上面,没错,问题的本质都回到原型上。

所以,事件回调收集,就是在原生上做手脚,改写原型上订阅和取消订阅的方法,两个点:

  1. 收集事件监听信息
  2. 保持原有功能

进一步的本质就是方法拦截, 那我们就一起再走近方法拦截

方法拦截

几种方法拦截的方式

方法拦截,我这里收集和整理了大约7种方式,当然有些类似,问题不大:

  • 简单重写原来的方法
  • 简单代理
  • 继承方式
  • 动态代理
  • ES6+ 标准的的 Proxy
  • ES5 标准的的 defineProperty
  • ES6+ 标准的的 decotator

具体每个方法的简单例子可以到这 几种方法拦截的方式

比较理想和通用的当然是后三种,

  • decotator
    显然这里不太合适,一是装饰器要 显式 的入侵代码,二是成本代价太高。

  • defineProperty
    非常直接和有效的方法,重定义 get,返回我们修改后的函数即可。
    不过,我就不,就不,我就喜欢玩Proxy.

  • Proxy
    其一:Proxy,返回的是一个新的对象,你需要使用这个新的对象,才能有效的代理。
    其二:我们做事要负责,要支持还原,所以更准确的说,我们要使用的是可取消的代理。 简单的代码就是如下:

const ep = EventTarget.prototype;
const rvAdd = Proxy.revocable(ep.addEventListener, this.handler);
ep.addEventListener = rvAdd.proxy;

如何收集DOM事件监听函数

我们拦截原型方法,本质就是为了收集事件监听函数。 其实除了拦截原型,也有一些方式可以获取到。

第三方库 getEventListeners

其只是直接修改了原型方法,并在节点上存储相关信息,结果可行,不推荐这么玩。

缺点

  1. 入侵了每个节点,节点上保留了事件信息
  2. 单次只能获取一个元素的监听事

chrome 控制台 getEventListeners方法获得单个Node的事件

缺点

  1. 只能在控制台使用
  2. 单次只能获取一个元素的监听事件

chrome控制台, Elements => Event Listeners

  1. 只能在开发者工具界面使用
  2. 查找相对麻烦

chrome more tools => Performance monitor 可以得到 JS event listeners, 也就是事件总数

并未有详细的信息,只有一个统计数据

数据结构和弱引用的问题

前面已经提到过了WeakRef,但是我们得思考,需要对哪些对象进行弱引用。

选择什么数据结构存储

既然是进行统计和分析,肯定要存储一些数据。
而这里我们是需要以对象作为键,因为我们要统计的是某个EventTarget或者EventEmitter的实例的事件订阅情况。

所以首选项, Map , Set, WeakMap, WeakSet,你会选择谁呢?
WeapMap和WeakSet看起来美好,但存在一个 很致命 的问题,那就是 不能进行遍历 。 不能遍历,当然就没法进行统计。

所以这里选择Map是比较合适的。

对那些数据弱引用

先罗列一下事件订阅和取消订阅需要设计到的数据:

window.addEventListener("message", this.onMessage, false);
emitter.on("event1", this.onEvent1);

对照代码分析:

  1. target
    事件挂载的对象,假如是EventEmitter,挂载的对象就是其实例
  2. type
    事件类型
  3. listener
    监听函数
  4. options
    选项,虽然EventEmitter没有,都没有,我们就认为一样即可。

选择的是对targetlistener进行弱引用,
, 大致的数据存储结构如下。

image.png

我们对事件从属的主体和事件回调函数进行弱引用,TypeScript表示为:

interface EventsMapItem {listener: WeakRef<Function>;options: TypeListenerOptions
}Map<WeakRef<Object>, Record<string, EventsMapItem[]>>

看似OK了,其实有个不容小视的问题,伴随着程序的支持运行, Map的Key的数量会增长,这个Key为WeakRef, WeakRef弱引用的对象可能已经被回收了,而与tartget关联的WeakRef并不会被回收。

当然,你可以周期性的去清理,也可以遍历的时候无视这些没有真实引用的WeakRef的Key

但是,不友好! 这里,就有请下一个主角 FinalizationRegistry

FinalizationRegistry

FinalizationRegistry 对象可以让你在对象被垃圾回收时请求一个回调。

看个简单的例子:

const registry = new FinalizationRegistry(name => {console.log(name,  " 被回收了");
});
var theObject = {name: '测试对象',
}
registry.register(theObject, theObject.name);
setTimeout(() => {window.gc();theObject = null;
}, 100);

image.png

对象被回收之后,如期的输出消息,这里, theObject = null不可以少,所以,确定对象不被使用之后,设置为null绝对不是一个坏习惯。

我们用FinalizationRegistry来监听对事件从属对象的回收,代码大致如下:

  #listenerRegistry = new FinalizationRegistry<{ weakRefTarget: WeakRef<object> }>(({ weakRefTarget }) => {console.log("evm::clean up ------------------");if (!weakRefTarget) {return;}this.eventsMap.remove(weakRefTarget);console.log("length", [...this.eventsMap.data.keys()].length);})

不难理解,因为Map的key就是WeakRef<object>, 所以target被回收之后,我们需要把与其关联的WeakRef也删除掉。

到此为止,我们可以收集对象和其注册的事件监听函数信息,有了数据,下一步就是预警,分析和统计。

如何判断重复添加的事件监听函数

基于EventTarget的事件订阅

先来一起看一段代码,请思考一下,按钮点击之后,输出了几次 clicked

<button id="btn1">点我啊</button>function onClick(){console.log("clicked");
}
const btnEl = document.getElementById("btn");btnEl.addEventListener("click", onClick);
btnEl.addEventListener("click", onClick);
btnEl.addEventListener("click", onClick);

答案是: 1次

因为EventTarget有天然去重的本领,具体参见 多个相同的事件处理器

你可能说你懂了,那我们稍微提升一下难度, 现在是几次呢??

    btnEl.addEventListener("click", onClick);btnEl.addEventListener("click", onClick, false);btnEl.addEventListener("click", onClick, {passive: false,});btnEl.addEventListener("click", onClick, {capture: false,});btnEl.addEventListener("click", onClick, {capture: false,once: true,});

答案: 还是 1次

其裁定是否相同回调函数的标准是:options中的capture的参数值一致capture默认值是false。

正因为这个特性,我们在拦截订阅函数的时候,需要进行判断,以免误收集。

如果是addEventListener返回的是布尔值,那倒是可以作为一个判断的依据,可惜的是返回的是undefined,天意,笑过,不哭。

到这里,有些人应该是笑了, 这不是不重复添加吗? 那又何来泄漏???

泄漏的根本来源

我开头提了一句 SPA更加剧了这种现象, 这种现象就是事件函数导致内存泄漏的现象。

// Hooks 也有同样问题
componentDidMount(){window.addEventListener("resize", this.onResize);
}
componentWillUnmount(){// 忘记remove EventListener
}

this.onResize是随着组件一起创建的,所以组件每创建一次,其也会被创建一次,虽然代码相同,但依旧是一个新的她。
组件销毁时,但是this.onResize时被 window 给引用了,并不能被销毁。

后果可想而知, 基于 EventEmitter的事件函数,也是同样的道理。 如果你有打日志的习惯,就会发现,疯狂输出的日志,你该庆幸, 发现了泄漏问题。

何为相同函数

如何判断相同函数,成为了我们的关键。

引用相同当然是,在基于EventTarget事件订阅体系下,是天然屏蔽的,而基于EventEmitter的订阅体系就没那么幸运了。

这时候,大家天天见,却也忽视不见的方法出场了, toString, 没错,是他,是他,是他,我们可爱的小toString

function fn(){console.log("a");
}
console.log(fn.toString()) 
// 输出::
// function fn(){
//    console.log("a");
//}

大家还有记得 玉伯 的 seajs 吗,其依赖查找,就是借助了toString

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

我们比内容,绝大情况是没问题了, 除了:

  1. 你的函数代码真就一样
    ESLint里面有一个规则,好像是没使用this的方法,是不该写在class里面的。真一样,你应该思考是的代码实现了。
  2. 内置函数
const random = Math.random
console.log("name:", random.name, ",content:", random.toString())
// name: random ,content: function random() { [native code] }
  1. 被bind的函数
function a(){console.log("name:", this.name)
}var b = a.bind({})
console.log("name:", b.name, ",content:", b.toString())
// name: bound a ,content: function () { [native code] }

所以我们检查内置函数和bind之后的函数,基本思路就是name{ [native code] }

问题大不,问题挺大,也就是说我们被bind之后的函数无法被比较了,就无法裁定是否是相同的函数了。

重写bind

答案就是重写bind,让其返回的函数有属性指向原函数,如果有更好的方式,请务必告诉我。

function log(this: any) {console.log("name:", this.name);
}var oriBind = Function.prototype.bind;
var SymbolOriBind = Symbol.for("__oriBind__");
Function.prototype.bind = function () {var f = oriBind.apply(this as any, arguments as any);f[SymbolOriBind] = this;return f;
}const boundLog: any = log.bind({ name: "哈哈" });
console.log(boundLog[SymbolOriBind].toString());//function log() {
//    console.log("name:", this.name);
//}

重写bind之后,必然会多了不安定元素, 所以:

  1. 也采用WeakRef来引用,减少不安定心理
  2. 默认不开启重写bind
    基本问题排查差不多了,再开启重写bind选项,仅仅分析被bind之后的事件监听函数。

怎么识别是不是被bind之后的函数,还是上面提到的

  1. 函数名, 其名为 bound [原函数名]
  2. 函数体, { [native code] }

小结

基本思路

  1. WeakRef建立和target对象的关联,并不影响其回收
  2. 重写 EventTargetEventEmitter 两个系列的订阅和取消订阅的相关方法, 收集事件注册信息
  3. FinalizationRegistry 监听 target回收,并清除相关数据
  4. 函数比对,除了引用比对,还有内容比对
  5. 对于bind之后的函数,采用重写bind方法来获取原方法代码内容

两个疑虑

  1. 兼容性
    是的,只能用在比较新的浏览器上调试。但是,问题不大! 发现并修复了,低版本大概率也修复了。

  2. 移动端怎么调试
    可以的,不是本文重点。

上面的几个问题分析完毕之后,我们完事具备,只欠东风。

敬请期待 代码篇

写在最后

技术交流群请到 这里来。
或者添加我的微信 dirge-cloud,带带我,一起学习。

EventTargetEventEmitter 两个系列的订阅和取消订阅的相关方法, 收集事件注册信息
3. FinalizationRegistry 监听 target回收,并清除相关数据
4. 函数比对,除了引用比对,还有内容比对
5. 对于bind之后的函数,采用重写bind方法来获取原方法代码内容

两个疑虑

  1. 兼容性
    是的,只能用在比较新的浏览器上调试。但是,问题不大! 发现并修复了,低版本大概率也修复了。

  2. 移动端怎么调试
    可以的,不是本文重点。

上面的几个问题分析完毕之后,我们完事具备,只欠东风。

敬请期待 代码篇

写在最后

技术交流群请到 这里来。
或者添加我的微信 dirge-cloud,带带我,一起学习。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/790349.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

大数据学习第十一天(复习linux指令3)

1、su和exit su命令就是用于账户切换的系统命令 基本语法&#xff1a;su[-] [用户名] 1&#xff09;-表示是否在切换用户后加载变量&#xff0c;建议带上 2&#xff09;参数&#xff1a;用户名&#xff0c;表示切换用户 3&#xff09;切换用户后&#xff0c;可以通过exit命令退…

Spring定义Bean对象笔记(二)

前言&#xff1a;上一篇记录了通过XML文件来定义Bean对象&#xff0c;这一篇将记录通过注解和配置类的方式来定义Bean对象。 核心注解&#xff1a; 定义对象&#xff1a;Component,Service,Repository,Controller 依赖注入&#xff1a; 按类型&#xff1a;Autowired 按名称&am…

【Unity每日一记】(Canvas的相机渲染模式) 如何将模型显示在UI之前

&#x1f468;‍&#x1f4bb;个人主页&#xff1a;元宇宙-秩沅 &#x1f468;‍&#x1f4bb; hallo 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍&#x1f4bb; 本文由 秩沅 原创 &#x1f468;‍&#x1f4bb; 收录于专栏&#xff1a;uni…

Stable Diffusion扩散模型推导公式的基础知识

文章目录 1、独立事件的条件概率2、贝叶斯公式、先验概率、后验概率、似然、证据3、马尔可夫链4、正态分布 / 高斯分布5、重参数化技巧6、期望7、KL散度 、高斯分布的KL散度8、极大似然估计9、ELBO :Evidence Lower Bound10、一元二次方程 1、独立事件的条件概率 A 和 B 是两个…

Java编程使用CGLIB动态代理介绍与实战演示

文章目录 前言技术积累核心概念主要功能适用场景与JDK动态代理的对比 实战演示定义待代理的目标类实现MethodInterceptor接口使用代理对象 测试结果写在最后 前言 在Java编程中&#xff0c;CGLIB (Code Generation Library) 是一个强大的高性能代码生成库&#xff0c;它通过生…

MySQL UPDATE JOIN 根据一张表或多表来更新另一张表的数据

当使用MySQL时&#xff0c;经常需要根据一张表或多张表的数据来更新另一张表的数据。这种情况下&#xff0c;我们可以使用UPDATE语句结合JOIN操作来实现这一需求。本文将介绍MySQL中使用UPDATE JOIN的技术。 什么是UPDATE JOIN UPDATE JOIN是MySQL中一种结合UPDATE语句和JOIN…

2024年第三期丨全国高校大数据与人工智能师资研修班邀请函

2024年第三期 杭州线下班 数据采集与机器学习实战&#xff08;Python&#xff09; 线上班 八大专题 大模型技术与应用实战 数据采集与处理实战&#xff08;Python&八爪鱼&#xff09; 大数据分析与机器学习实战&#xff08;Python&#xff09; 商务数据分析实战&…

jQuery(一)

文章目录 1. 基本介绍2.原理示意图3.快速入门1.下载jQuery2.创建文件夹&#xff0c;放入jQuery3.引入jQuery4.代码实例 4.jQuery对象与DOM对象转换1.基本介绍2.dom对象转换JQuery对象3.JQuery对象转换dom对象4.jQuery对象获取数据获取value使用val&#xff08;&#xff09;获取…

完全没想到docker启动败在了这里!

转载说明&#xff1a;如果您喜欢这篇文章并打算转载它&#xff0c;请私信作者取得授权。感谢您喜爱本文&#xff0c;请文明转载&#xff0c;谢谢。 故事背景 前几天帮同事部署一个环境&#xff0c;用他写的安装脚本部署&#xff0c;其中一台服务器就需要安装docker&#xff0c…

基于深度学习的铁轨缺陷检测系统(网页版+YOLOv8/v7/v6/v5代码+训练数据集)

摘要&#xff1a;本文深入研究了基于YOLOv8/v7/v6/v5的铁轨缺陷检测系统。核心技术上&#xff0c;文章采用了最先进的YOLOv8&#xff0c;并整合了YOLOv7、YOLOv6、YOLOv5算法&#xff0c;进行了性能指标的对比分析。文中详细阐述了国内外铁轨缺陷检测的研究现状、数据集处理方法…

MHA高可用-解决MySQL主从复制的单点问题

目录 一、MHA的介绍 1&#xff0e;什么是 MHA 2&#xff0e;MHA 的组成 2.1 MHA Node&#xff08;数据节点&#xff09; 2.2 MHA Manager&#xff08;管理节点&#xff09; 3&#xff0e;MHA 的特点 4. MHA工作原理总结如下&#xff1a; 二、搭建 MySQL MHA 实验环境 …

【LeetCode热题100】【普通数组】合并区间

题目链接&#xff1a;56. 合并区间 - 力扣&#xff08;LeetCode&#xff09; 先排序&#xff0c;按左区排序&#xff0c;装第一个区间进入答案容器&#xff0c;判断答案容器钟最后一个区间的右区是否小于区间的左区&#xff0c;是则不能合并是新区间&#xff0c;否则可以合并 …

反转链表1

/*** 代码中的类名、方法名、参数名已经指定&#xff0c;请勿修改&#xff0c;直接返回方法规定的值即可*** param head ListNode类* return ListNode类*/ListNode* ReverseList(ListNode* head) {// write code hereif (headNULL) {return nullptr;}ListNode* start(ListNode*…

UITabBarController管理FBFlutterViewContainer首次页面空白

UITabBarController管理FBFlutterViewContainer首次页面空白 可能是因为在使用UITabBarController管理FBFlutterViewContainer时&#xff0c;初始的页面没有正确加载或渲染。FBFlutterViewContainer可能是Flutter的一个视图容器&#xff0c;而在iOS开发中&#xff0c;UITabBar…

[蓝桥杯练习题]出差

一道DJ题,重要的是隔离时间,把隔离时间加在边权上即可 现实生活的题大多都是无向图建图,需要边的两端点各自上邻接表和相同权重 #include<bits/stdc.h> using namespace std; #define ll long long const int N1005; const int M10005; struct edge{int to;ll w;edge(int…

MySQL数据库 数据库基本操作(一):数据库的认识与基本操作

1. 数据库的基本认识 1.1 什么是数据库 专家们设计出更加利于管理数据的软件——数据库&#xff0c;它能更有效的管理数据。数据库可以提供远程服务&#xff0c;即通过远程连接来使用数据库&#xff0c;因此也称为数据库服务器。 1.2 数据库的分类 数据库可以大体分为:关系…

Positive Technologies:2023 年,三分之一针对零售商的攻击会导致销售中断

根据 Positive Technologies 的研究&#xff0c;零售商和电子商务公司一直是黑客关注的焦点&#xff0c;在报告的被盗数据和影子市场的基础设施访问方面&#xff0c;零售商和电子商务公司排名前三。与此同时&#xff0c;80% 的零售商广告提供免费赠送被盗数据库的服务。 到 20…

单片机家电产品--过零检测

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 单片机家电产品–过零检测 前言 记录学习单片机家电产品内容 已转载记录为主 一、知识点 1 什么是过零检测 1 过零检测指的是在交流系统中&#xff0c;在一个交流周期中…

Redis 的主从复制、哨兵

目录 一. Redis 主从复制 1. 介绍 2. 作用 3. 流程 4. 搭建 Redis 主从复制 安装redis 修改 master 的Redis配置文件 修改 slave 的Redis配置文件 验证主从效果 二. Redis 哨兵模式 1. 介绍 2. 原理 3. 哨兵模式的作用 4. 工作流程 4.1 故障转移机制 4.2 主节…

装饰工程管理系统|基于Springboot的装饰工程管理系统设计与实现(源码+数据库+文档)

装饰工程管理系统-项目立项子系统目录 目录 基于Springboot的装饰工程管理系统设计与实现 一、前言 二、系统功能设计 三、系统实现 1、管理员功能实现 &#xff08;2&#xff09;合同报价管理 &#xff08;3&#xff09;装饰材料总计划管理 &#xff08;4&#xff0…