事件监听函数的内存泄漏,都给我退散吧!

[[427730]]

本文转载自微信公众号「云的程序世界」,作者云的世界。转载本文请联系云的程序世界公众号。

前言

内存泄漏是个很严肃的问题,可是迄今也没有一个非常有效的排查方案,本方案就是针对性的单点突破。

工作中,我们会对window, DOM节点,WebSoket, 或者单纯的事件中心等注册事件监听函数, 添加了,没有移除,就会导致内存泄漏,如何预警,收集,排查这种问题呢?

本文是代码篇,主要讲使用和实现。

源码和demo

源码:事件分析vem[2]

项目内部有丰富的例子。

核心功能

我们解决问题的时机无非为 事前, 事中, 事后。

我们这里主要是 事前 和 事后。

  • 事件监听函数添加前进行预警
  • 事件监听函数添加后进行统计

了解功能之前,先了解一下四同特性:

1.同一事件监听函数从属对象

事件监听总是要注册到响应的对象上的, 比如下面代码的window, socket, emitter都是事件监听函数的从属对象、

  1. window.addEventListener("resize",onResize) 
  2.  
  3. socket.on("message", onMessage); 
  4.  
  5. emitter.on("message", onMessage); 

2.同一事件监听函数类型

这个比较好理解,比如window的 message, resize等,Audio的 play等等

3.同一事件监听函数内容

这里注意一点,事件监听函数相同,分两种:

  • 函数引用相同
  • 函数内容相同

4.同一事件监听函数选项

这个可选项,EventTarget系列有这些选项,其他系列没有。

选项不同,添加和删除的时候结果就可能不通。

  1. window.addEventListener("resize",onResize) 
  2. // 移除事件监听函数onResize失败 
  3. window.removeEventListener("resize",onResize, true

预警

事件监听函数添加前,比对四同属性的事件监听函数,如果有重复,进行报警。

统计高危监听事件函数

最核心的功能。

统计事件监听函数从属对象的所有事件信息,输出满足 四同属性 的事件监听函数。如果有数据输出,极大概率,你内存泄漏了。

统计全部的事件监听函数

统计事件监听函数从属对象的所有事件信息, 可以用于分析业务逻辑。

一览你添加了多少事件, 是不是有些应该不存的,还存在呢?

基本使用

初始化参数

内置三个系列:

  1. new EVM.ETargetEVM(options, et);  //  EventTarget系列 
  2. new EVM.EventsEVM(options, et);   //  events 系列 
  3. new EVM.CEventsEVM(options, et);  // component-emitter系列 

当然,你可以继承BaseEvm, 自定义出新的系列,因为上面的三个系列也都是继承BaseEvm而来。

最主要的初始化参数也就是 options

  • options.isSameOptions

是一个函数。主要是用来判定事件监听函数的选项。

  • options.isInWhiteList

是一个函数。主要用来判定是否收集。

  • options.maxContentLength

是一个数字。你可以限定统计时,需要截取的函数内容的长度。

EventTarget系列

  • EventTarget[3]
  • DOM节点 + windwow + document
  • XMLHttpRequest 其继承于 EventTarget
  • 原生的WebSocket 其继承于 EventTarget
  • 其他继承自EventTarget的对象

基本使用

  1. <script src="http://127.0.0.1:8080/dist/evm.js?t=5"></script> 
  2. <script> 
  3.     const evm = new EVM.ETargetEVM({ 
  4.         // 白名单,因为DOM事件的注册可能 
  5.         isInWhiteList(target, event, listener, options) { 
  6.             if (target === window && event !== "error") { 
  7.                 return true
  8.             } 
  9.             return false
  10.          } 
  11.     }); 
  12.     // 开始监听 
  13.     evm.watch(); 
  14.  
  15.     // 定期打印极有可能是重复注册的事件监听函数信息 
  16.     setInterval(async function () { 
  17.         // statistics getExtremelyItems 
  18.         const data = await evm.getExtremelyItems({ containsContent: true }); 
  19.         console.log("evm:", data); 
  20.     }, 3000) 
  21. </script> 

 

 

效果截图

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

events[4] 系列

  • Nodejs 标准的 events[5]
  • MQTT 基于 events[6]库
  • socket.io 基于 events[7]库

基本使用

  1. import { EventEmitter } from "events"
  2.  
  3. const evm = new win.EVM.EventsEVM(undefined, EventEmitter); 
  4. evm.watch(); 
  5. setTimeout(async function () { 
  6.     // statistics getExtremelyItems 
  7.     const data = await evm.getExtremelyItems(); 
  8.     console.log("evm:", data); 
  9. }, 5000) 

效果截图

截图来自我对实际项目的分析 ,APP_ACT_COM_HIDE_ 系列事件重复添加

component-emitter[8] 系列

  • component-emitter
  • socket.io-client(即socket.io的客户端)

基本使用

  1. const Emitter = require('component-emitter'); 
  2. const emitter = new Emitter(); 
  3.  
  4. const EVM = require('../../dist/evm'); 
  5.  
  6. const evm = new EVM.CEventsEVM(undefined, Emitter); 
  7. evm.watch(); 
  8.  
  9. // 其他代码 
  10.  
  11. evm.getExtremelyItems() 
  12.     .then(function (res) { 
  13.         console.log("res:", res.length); 
  14.         res.forEach(r => { 
  15.             console.log(r.type, r.constructor, r.events); 
  16.         }) 
  17.     }) 

效果截图

事件分析的基本思路

上篇总结的思路:

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

对于bind之后的函数,采用重写bind方法来获取原方法代码内容

代码结构

代码基本结构如下:

具体注释如下:

  1. evm 
  2.     CEvents.ts // components-emitter系列,继承自 BaseEvm 
  3.     ETarget.ts // EventTarget系列,继承自 BaseEvm 
  4.     Events.ts  // events系列,继承自 BaseEvm 
  5. BaseEvm.ts  // 核心逻辑类 
  6. custom.d.ts  
  7. EventEmitter.ts // 简单的事件中心 
  8. EventsMap.ts // 数据存储的核心 
  9. index.ts // 入口文件 
  10. types.ts // 类型申请 
  11. util.ts // 工具类 

核心实现

EventsMap.ts

负责数据的存储和基本的统计。

数据存储结构:(双层Map)

  1.  Map<WeakRef<Object>, Map<EventType, EventsMapItem<T>[]>>(); 
  2.   
  3. interface EventsMapItem<O = any> { 
  4.     listener: WeakRef<Function>; 
  5.     options: O 

内部结构的大纲如下:

方法都很好理解,大家可能注意到了,有些方法后面跟着byTarget的字样,那是因为 其内部采用Map存储,但是key的类型是弱引用WeakRef。

我们增加和删除事件监听的时候,传入的对象肯定是普通的target对象,需要多经过一个步骤,通过target来查到其对应的key,这就是byTarget要表达的意思。

还是罗列一些方法的作用:

  • getKeyFromTarget

通过target对象获得键

  • keys

获得所有弱引用的键值

  • addListener

添加监听函数

  • removeListener

删除监听函数

  • remove

删除某个键的所有数据

  • removeByTarget

通过target删除某个键的所有数据

  • removeEventsByTarget

通过target删除某个键某个事件类型的所有数据

  • hasByTarget

通过target查询是否有某个键

  • has

是否有某个键

  • getEventsObj

获得某个target的所有事件信息

  • hasListener

某个target是否存在某个事件监听函数

  • getExtremelyItems

获得高危的事件监听函数信息

  • get data

获得数据

BaseEVM

内部结构的大纲如下:

核心实现就是watch和cancel,继承BaseEVM并重写这两个方法,你就可以获得一个新的系列。

统计的两个核心方法就是 statistics 和 getExtremelyItems。

还是罗列一些方法的作用:

  • innerAddCallback

监听事件函数的添加,并收集相关信息

  • innerRemoveCallback

监听事件函数的添加,并清理相关信息

  • checkAndProxy

检查并执行代理

  • restoreProperties

恢复被代理属性

  • gc

如果可以,执行垃圾回收

  • #getListenerContent

统计时,获取函数内容

  • #getListenerInfo

统计时,获得函数信息,主要是name和content。

  • statistics

统计所有事件监听函数信息。

  • #getExtremelyListeners

统计高危事件

  • getExtremelyItems

基于#getExtremelyListeners汇总高危事件信息。

  • watch

执行监听,需要被重写的方法

  • cancel

取消监听,需要被重写的方法

  • removeByTarget

清理某个对象的所有数据

  • removeEventsByTarget

清理某个对象某类类型的事件监听

ETargetEVM

我们已经提到过,实际上已经实现了三个系列,我们就以ETargetEVM为例,看看怎么通过继承和重写获得对某个系列事件监听的收集和统计。

核心就是重写watch和cancel,分别对应了代理和取消相关代理

checkAndProxy是核心,其封装了代理过程, 通过自定义第二个参数(函数),过滤数据。

就这么简单

  1. const DEFAULT_OPTIONS: BaseEvmOptions = { 
  2.     isInWhiteList: boolenFalse, 
  3.     isSameOptions: isSameETOptions 
  4.  
  5. const ADD_PROPERTIES = ["addEventListener"]; 
  6. const REMOVE_PROPERTIES = ["removeEventListener"]; 
  7.  
  8. /** 
  9.  * EVM for EventTarget 
  10.  */ 
  11. export default class ETargetEVM extends BaseEvm<TypeListenerOptions> { 
  12.  
  13.     protected orgEt: any
  14.     protected rpList: { 
  15.         proxy: object; 
  16.         revoke: () => void; 
  17.     }[] = []; 
  18.     protected et: any
  19.  
  20.     constructor(options: BaseEvmOptions = DEFAULT_OPTIONS, et: any = EventTarget) { 
  21.         super({ 
  22.             ...DEFAULT_OPTIONS, 
  23.             ...options 
  24.         }); 
  25.  
  26.         if (et == null || !isObject(et.prototype)) { 
  27.             throw new Error("参数et的原型必须是一个有效的对象"
  28.         } 
  29.         this.orgEt = { ...et }; 
  30.         this.et = et; 
  31.  
  32.     } 
  33.  
  34.     #getListenr(listener: Function | ListenerWrapper) { 
  35.         if (typeof listener == "function") { 
  36.             return listener 
  37.         } 
  38.         return null
  39.     } 
  40.  
  41.     #innerAddCallback: EVMBaseEventListener<void, string> = (target, event, listener, options) => { 
  42.         const fn = this.#getListenr(listener) 
  43.         if (!isFunction(fn as Function)) { 
  44.             return
  45.         } 
  46.         return super.innerAddCallback(target, event, fn as Function, options); 
  47.     } 
  48.  
  49.     #innerRemoveCallback: EVMBaseEventListener<void, string> = (target, event, listener, options) => { 
  50.         const fn = this.#getListenr(listener) 
  51.         if (!isFunction(fn as Function)) { 
  52.             return
  53.         } 
  54.         return super.innerRemoveCallback(target, event, fn as Function, options); 
  55.     } 
  56.  
  57.  
  58.     watch() { 
  59.         super.watch(); 
  60.         let rp; 
  61.         // addEventListener  
  62.         rp = this.checkAndProxy(this.et.prototype, this.#innerAddCallback, ADD_PROPERTIES); 
  63.         if (rp !== null) { 
  64.             this.rpList.push(rp); 
  65.         } 
  66.         // removeEventListener 
  67.         rp = this.checkAndProxy(this.et.prototype, this.#innerRemoveCallback, REMOVE_PROPERTIES); 
  68.         if (rp !== null) { 
  69.             this.rpList.push(rp); 
  70.         } 
  71.  
  72.         return () => this.cancel(); 
  73.     } 
  74.  
  75.     cancel() { 
  76.         super.cancel(); 
  77.         this.restoreProperties(this.et.prototype, this.orgEt.prototype, ADD_PROPERTIES); 
  78.         this.restoreProperties(this.et.prototype, this.orgEt.prototype, REMOVE_PROPERTIES); 
  79.         this.rpList.forEach(rp => rp.revoke()); 
  80.         this.rpList = []; 
  81.     } 

总结

  • 单独设计了一套存储结构EventsMap
  • 把基础的逻辑封装在BaseEVM
  • 通过继承重写某些方法,从而可以满足不同的事件监场景。

 

文章来源网络,作者:运维,如若转载,请注明出处:https://shuyeidc.com/wp/136378.html<

(0)
运维的头像运维
上一篇2025-03-02 01:38
下一篇 2025-03-02 01:39

相关推荐

  • 个人主题怎么制作?

    制作个人主题是一个将个人风格、兴趣或专业领域转化为视觉化或结构化内容的过程,无论是用于个人博客、作品集、社交媒体账号还是品牌形象,核心都是围绕“个人特色”展开,以下从定位、内容规划、视觉设计、技术实现四个维度,详细拆解制作个人主题的完整流程,明确主题定位:找到个人特色的核心主题定位是所有工作的起点,需要先回答……

    2025-11-20
    0
  • 社群营销管理关键是什么?

    社群营销的核心在于通过建立有温度、有价值、有归属感的社群,实现用户留存、转化和品牌传播,其管理需贯穿“目标定位-内容运营-用户互动-数据驱动-风险控制”全流程,以下从五个维度展开详细说明:明确社群定位与目标社群管理的首要任务是精准定位,需明确社群的核心价值(如行业交流、产品使用指导、兴趣分享等)、目标用户画像……

    2025-11-20
    0
  • 香港公司网站备案需要什么材料?

    香港公司进行网站备案是一个涉及多部门协调、流程相对严谨的过程,尤其需兼顾中国内地与香港两地的监管要求,由于香港公司注册地与中国内地不同,其网站若主要服务内地用户或使用内地服务器,需根据服务器位置、网站内容性质等,选择对应的备案路径(如工信部ICP备案或公安备案),以下从备案主体资格、流程步骤、材料准备、注意事项……

    2025-11-20
    0
  • 如何企业上云推广

    企业上云已成为数字化转型的核心战略,但推广过程中需结合行业特性、企业痛点与市场需求,构建系统性、多维度的推广体系,以下从市场定位、策略设计、执行落地及效果优化四个维度,详细拆解企业上云推广的实践路径,精准定位:明确目标企业与核心价值企业上云并非“一刀切”的方案,需先锁定目标客户群体,提炼差异化价值主张,客户分层……

    2025-11-20
    0
  • PS设计搜索框的实用技巧有哪些?

    在PS中设计一个美观且功能性的搜索框需要结合创意构思、视觉设计和用户体验考量,以下从设计思路、制作步骤、细节优化及交互预览等方面详细说明,帮助打造符合需求的搜索框,设计前的规划明确使用场景:根据网站或APP的整体风格确定搜索框的调性,例如极简风适合细线条和纯色,科技感适合渐变和发光效果,电商类则可能需要突出搜索……

    2025-11-20
    0

发表回复

您的邮箱地址不会被公开。必填项已用 * 标注