[Код] Утечка памяти в функции слушателя событий, отпустите меня!

внешний интерфейс JavaScript
[Код] Утечка памяти в функции слушателя событий, отпустите меня!

Эта статья приняла участие"Проект "Звезда раскопок"", чтобы выиграть творческие подарочные пакеты и бросить вызов творческим поощрениям.

предисловие

Утечка памяти — очень серьезная проблема, но до сих пор не было очень эффективного решения для устранения неполадок, это решение представляет собой целенаправленный одноточечный прорыв.

На работе мы будемwindowDOMузел,WebSoketили просто事件中心Когда зарегистрированная функция прослушивания событий добавляется, но не удаляется, это приведет к утечке памяти.如何预警,收集,排查这种问题呢?

Эта статья представляет собой статью о коде, в основном об использовании и реализации.

Для получения дополнительных теоретических знаний, пожалуйста, прочитайте главу теории[Программа] Утечка памяти функции слушателя событий вам в помощь!

Исходный код и демо

Исходный код:анализ событий

Внутри проекта есть богатые примеры.

Основные функции

мы решаем проблемы时机не более, чемзаранее,в середине,после.

Мы в основном здесь事前а также事后.

  • Предупреждение перед добавлением функции прослушивания событий
  • Статистика после добавления функции прослушивания событий

Прежде чем понять функцию, давайте сначала разберемся с четырьмя характеристиками:

  1. Зависимый объект той же функции прослушивателя событий
    Слушатели событий всегда должны быть зарегистрированы в объекте ответа, например, в следующем коде.window, socket, emitterвсе подчиненные объекты функции прослушивателя событий,

    window.addEventListener("resize",onResize)
    
    socket.on("message", onMessage);
    
    emitter.on("message", onMessage);
    
  2. Тот же тип функции прослушивателя событий
    Это легче понять, например, окноmessageresizeПодождите, аудиоplayтак далее

  3. Содержимое той же функции прослушивателя событий
    Здесь обратите внимание, функция прослушивания событий такая же, разделенная на два типа:

    • ссылка на функцию такая же
    • Содержание функции такое же
  4. Те же параметры функции прослушивателя событий
    этот вариант,EventTargetУ серий есть эти опции, у других серий нет.
    Варианты разные, и результаты могут отличаться при добавлении и удалении.

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

раннее предупреждение

Перед добавлением функции прослушивания событий сравнитеЧетыре одинаковых свойстваФункция прослушивания событий , если есть повторение, это подаст сигнал тревоги.

Функция статистического мониторинга событий высокого риска

Самая основная функция.
Подсчитайте всю информацию о событии подчиненного объекта функции прослушивателя событий, и выходные данные удовлетворятЧетыре одинаковых свойствафункция прослушивания событий.如果有数据输出,极大概率,你内存泄漏了。

Подсчет всех функций прослушивателя событий

Подсчитайте всю информацию о событиях подчиненных объектов функции слушателя событий, которые можно использовать для анализа бизнес-логики.

с первого взгляда, сколько событий вы добавили,是不是有些应该不存的,还存在呢?

основное использование

Параметры инициализации

Встроенные три серии:

новый EVM.ETargetEVM(options, et);//серия EventTarget
new EVM.EventsEVM(options, et);// серия событий
new EVM.CEventsEVM(options, et);//серия компонент-эмиттер

Конечно, вы можете унаследоватьBaseEvm, Настройте новую серию, потому что три вышеуказанные серии также унаследованыBaseEvmприходить.

Наиболее важным параметром инициализации являетсяoptions

  • options.isSameOptions
    является функцией. В основном используется для определения параметров функции прослушивателя событий.
  • options.isInWhiteList
    является функцией. В основном используется для определения необходимости сбора.
  • options.maxContentLength
    это число. Вы можете ограничить длину содержимого функции, которое необходимо перехватывать во время статистики.

Серия EventTarget

  • EventTarget
  • узел DOM + окно + документ
  • XMLHttpRequest, который наследуется от EventTarget
  • Собственный WebSocket, который наследуется от EventTarget
  • Другие объекты, наследуемые от EventTarget

основное использование

<script src="http://127.0.0.1:8080/dist/evm.js?t=5"></script>
<script>
    const evm = new EVM.ETargetEVM({
        // 白名单,因为DOM事件的注册可能
        isInWhiteList(target, event, listener, options) {
            if (target === window && event !== "error") {
                return true;
            }
            return false;
         }
    });
    // 开始监听
    evm.watch();

    // 定期打印极有可能是重复注册的事件监听函数信息
    setInterval(async function () {
        // statistics getExtremelyItems
        const data = await evm.getExtremelyItems({ containsContent: true });
        console.log("evm:", data);
    }, 3000)
</script>

Скриншот эффекта

截图来自我对实际项目的分析, на объекте окнаДублирование добавления сообщения сообщения, до 10 разimage.png

eventsряд

  • Стандарт Nodejsevents
  • MQTT основан наeventsбиблиотека
  • socket.ioна основеeventsбиблиотека

основное использование

import { EventEmitter } from "events";

const evm = new win.EVM.EventsEVM(undefined, EventEmitter);
evm.watch();
setTimeout(async function () {
    // statistics getExtremelyItems
    const data = await evm.getExtremelyItems();
    console.log("evm:", data);
}, 5000)

Скриншот эффекта

截图来自我对实际项目的分析 ,APP_ACT_COM_HIDE_Повторное добавление событий серииimage.png

component-emitterряд

  • component-emitter
  • socket.io-client (т.е. клиент socket.io)

основное использование

const Emitter = require('component-emitter');
const emitter = new Emitter();

const EVM = require('../../dist/evm');

const evm = new EVM.CEventsEVM(undefined, Emitter);
evm.watch();

// 其他代码

evm.getExtremelyItems()
    .then(function (res) {
        console.log("res:", res.length);
        res.forEach(r => {
            console.log(r.type, r.constructor, r.events);
        })
    })

Скриншот эффекта

image.png

Основная идея анализа событий

Идея предыдущего резюме:

  1. WeakRefпостроить иtargetАссоциация объекта не влияет на его переработку
  2. переписатьEventTargetа такжеEventEmitterСвязанные методы подписки и отказа от подписки на две серии, сбор информации о регистрации событий
  3. Монитор финализации реестраtargetПовторно использовать и очистить связанные данные
  4. Сравнение функций, в дополнение к сравнению ссылок, есть также сравнение контента
  5. Для функции после привязки перепишите метод привязки, чтобы получить содержимое кода исходного метода.

структура кода

Базовая структура кода выглядит следующим образом:

image.png

Конкретные примечания заключаются в следующем:

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

основная реализация

EventsMap.ts

Отвечает за хранение данных и основную статистику.

Структура хранения данных: (двойная карта)

 Map<WeakRef<Object>, Map<EventType, EventsMapItem<T>[]>>();
 
interface EventsMapItem<O = any> {
    listener: WeakRef<Function>;
    options: O
}

Схема внутренней структуры выглядит следующим образом:image.png

Методы хорошо понятны.Вы могли заметить, что за некоторыми методами следуютbyTarget, это потому что Он использует внутреннее хранилище карт, но тип ключа является слабой ссылкой.WeakRef.

Когда мы добавляем и удаляем прослушиватели событий, входящие объекты должны быть обычнымиtargetобъект, вам нужно пройти еще один шаг, черезtargetЧтобы найти соответствующий ключ, этоbyTargetсмысл выражать.

Или перечислите некоторые методы:

  • getKeyFromTarget
    Получить ключ от целевого объекта
  • keys
    Получить все ключи со слабыми ссылками
  • addListener
    Добавить функцию прослушивания
  • removeListener
    удалить функцию прослушивания
  • remove
    удалить все данные для ключа
  • removeByTarget
    Удалить все данные ключа по цели
  • removeEventsByTarget
    Удалить все данные определенного ключа и определенного типа события через цель
  • hasByTarget
    Запрос, есть ли определенный ключ по цели
  • has
    есть ли ключ
  • getEventsObj
    Получить всю информацию о событии цели
  • hasListener
    Есть ли функция прослушивания событий для цели
  • getExtremelyItems
    Получить информацию о функции прослушивателя событий с высоким риском
  • get data
    получить данные

BaseEVM

Схема внутренней структуры выглядит следующим образом:

image.png

Основная реализацияwatchа такжеcancel, наследуя BaseEVM и переопределяя эти два метода, можно получить новую серию.

Два основных метода статистики:statisticsа такжеgetExtremelyItems.

Или перечислите некоторые методы:

  • innerAddCallback
    Отслеживайте добавление функций событий и собирайте соответствующую информацию
  • innerRemoveCallback
    Отслеживайте добавление функций событий и очищайте связанную информацию
  • checkAndProxy
    Проверить и выполнить прокси
  • restoreProperties
    Восстановление проксируемых свойств
  • gc
    Если возможно, выполните сборку мусора
  • #getListenerContent
    Когда статистика, получить содержание функции
  • #getListenerInfo
    Во время статистики получают информацию о функциях, в основном имя и содержание.
  • statistics
    Статистика всех функций прослушивания событий.
  • #getExtremelyListeners
    Статистика событий высокого риска
  • getExtremelyItems
    Совокупная информация о событиях высокого риска на основе #getExtremelyListeners.
  • watch
    Выполнение мониторинга, методы, которые необходимо переопределить
  • cancel
    Отмените мониторинг, метод, который необходимо переопределить
  • removeByTarget
    Очистить все данные объекта
  • removeEventsByTarget
    Очистить прослушиватели событий определенного типа объекта

ETargetEVM

Мы уже упоминали, что фактически реализовано три серии, и начнем сETargetEVMНапример, давайте посмотрим, как получить коллекцию и статистику ряда прослушивателей событий посредством наследования и перезаписи.

  1. Суть в том, чтобы переписать наблюдение и отмену, которые соответствуют агентам и отменяют связанные агенты соответственно.
  2. checkAndProxyЯвляется ядром, которое инкапсулирует прокси-процесс и фильтрует данные, настраивая второй параметр (функцию).
  3. Это так просто
const DEFAULT_OPTIONS: BaseEvmOptions = {
    isInWhiteList: boolenFalse,
    isSameOptions: isSameETOptions
}

const ADD_PROPERTIES = ["addEventListener"];
const REMOVE_PROPERTIES = ["removeEventListener"];

/**
 * EVM for EventTarget
 */
export default class ETargetEVM extends BaseEvm<TypeListenerOptions> {

    protected orgEt: any;
    protected rpList: {
        proxy: object;
        revoke: () => void;
    }[] = [];
    protected et: any;

    constructor(options: BaseEvmOptions = DEFAULT_OPTIONS, et: any = EventTarget) {
        super({
            ...DEFAULT_OPTIONS,
            ...options
        });

        if (et == null || !isObject(et.prototype)) {
            throw new Error("参数et的原型必须是一个有效的对象")
        }
        this.orgEt = { ...et };
        this.et = et;

    }

    #getListenr(listener: Function | ListenerWrapper) {
        if (typeof listener == "function") {
            return listener
        }
        return null;
    }

    #innerAddCallback: EVMBaseEventListener<void, string> = (target, event, listener, options) => {
        const fn = this.#getListenr(listener)
        if (!isFunction(fn as Function)) {
            return;
        }
        return super.innerAddCallback(target, event, fn as Function, options);
    }

    #innerRemoveCallback: EVMBaseEventListener<void, string> = (target, event, listener, options) => {
        const fn = this.#getListenr(listener)
        if (!isFunction(fn as Function)) {
            return;
        }
        return super.innerRemoveCallback(target, event, fn as Function, options);
    }


    watch() {
        super.watch();
        let rp;
        // addEventListener 
        rp = this.checkAndProxy(this.et.prototype, this.#innerAddCallback, ADD_PROPERTIES);
        if (rp !== null) {
            this.rpList.push(rp);
        }
        // removeEventListener
        rp = this.checkAndProxy(this.et.prototype, this.#innerRemoveCallback, REMOVE_PROPERTIES);
        if (rp !== null) {
            this.rpList.push(rp);
        }

        return () => this.cancel();
    }

    cancel() {
        super.cancel();
        this.restoreProperties(this.et.prototype, this.orgEt.prototype, ADD_PROPERTIES);
        this.restoreProperties(this.et.prototype, this.orgEt.prototype, REMOVE_PROPERTIES);
        this.rpList.forEach(rp => rp.revoke());
        this.rpList = [];
    }
}

Суммировать

  • Отдельно разработан комплект складской конструкцииEventsMap
  • Инкапсулируйте основную логику вBaseEVM
  • Путем наследования и переопределения некоторых методов можно удовлетворить различные сценарии мониторинга событий.

напиши в конце

Пожалуйста, посетите группу технического обменаиди сюда. Или добавьте мое WeChat-похоронное облако, возьмите меня и учитесь вместе.