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

внешний интерфейс Шаблоны проектирования JavaScript
[Программа] Утечка памяти функции слушателя событий вам в помощь!

Эта статьяТеория, и следующийСтатьи кода.

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

предисловие

На работе мы будем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 в окне регистрируется событие мониторинга, а компонент выгружается, а не удаляется, что, скорее всего, выйдет из-под контроля.

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

Наша сегодняшняя темаАнализируйте прослушиватели событий и устраняйте возможные утечки памяти.

В этой статье в основном обсуждаются и анализируются несколько технических моментов:

  1. Как точно узнать, был ли объект или функция переработана
  2. Суть общих функций прослушивания событий
  3. Как собрать функции прослушивания событий DOM
  4. Распространенные способы перехвата методов
  5. Слабые ссылки на рециркуляцию проблемы
  6. Как разрешить повторение функции прослушивателя событий

Демонстрация эффекта

Вызовите полицию,Статистика событий высокого риска,Статистика событийДождитесь функции, посмотрим на эффект.

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

При регистрации на мероприятие, если вы обнаружитеЧетыре одинаковых свойстваМониторинг событий будет подан сигнал тревоги.
Ситонг:

  1. тот же объект, зависящий от события
    НапримерWindow, Socketэквивалентно экземпляру
  2. тип события, напримерmessage, resize,playтак далее
  3. функция обратного вызова события
  4. Параметры функции обратного вызова события

截图来自我对实际项目的分析, событие сообщения добавляется повторно, предупреждение! !

image.png

Статистика высокого риска

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

DOM-события

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

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

Статистика событий

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

так,

Большие Братья, Большие Братья, Большие Братья, обязательно назовите функцию, обязательно назовите функцию, обязательно назовите функцию.,

На этот раз имя действительно имеет значение! ! ! ! ! ! !

Как точно узнать, был ли объект или функция переработана

Интуитивно верный ответ: слабые ссылкиWeakRef+ GC (сборка мусора)

Почему слабые ссылки?Потому что мы не можем повлиять на переработку предметов из-за нашего анализа и статистики? В противном случае анализ точно не будет точным.

слабая ссылка

WeakRefОн предложен ES2021 и используется для прямого создания слабых ссылок на объекты, что не помешает очистке исходных объектов механизмом сборки мусора.

Объекты экземпляра WeakRef имеютderef()метод, если исходный объект существует, метод возвращает исходный объект; если исходный объект был очищен механизмом сборки мусора, метод возвращаетundefined.

let target = {};
let wr = new WeakRef(target);

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

Давайте посмотрим на практический пример:
Цель слева не будет переработана, а цель справа будет переработана. image.png

Видя это, вы должны иметь как минимум два сознания:

  1. window.gc()Что за черт
    Это метод, предоставляемый v8, который активно запускает сборку мусора, о чем будет сказано далее.
  2. IIFEПрименение этого затвора действительно может в определенной степени уменьшить переменное загрязнение и утечку.

вывоз мусора

Сборка мусора имеет циклы, взяв за пример браузер Chrome, он может активно выполнять сборку мусора. Если объект, подлежащий переработке, все еще находится там после активного выполнения операции по переработке, это, скорее всего, вызовет утечку.

Как браузер на движке v8 может активно выполнять сборку мусора?
Ответ: изменить параметры запуска хром, плюс--js-flags="--expose-gc"Только что

image.png

После этого вы можете просто позвонить напрямуюgcметод сбора мусора

image.png

резюме

имеютWeakRef+ Активный ГК
Затем вы можете протестировать и устранить неполадки там, где, по вашему мнению, могут быть утечки или загрязнения.

Увидеть суть через явление

Внешний вид прослушивателей событий

Возвращаясь к теме, сегодня мы сосредоточимся на функции обратного вызова события, Наши типы подписки на события, которые часто встречаются в веб-программированиивнешностьимеют:

  • DOM-события
    В основном события уровня DOM2, т.е.addEventListener, removeEventListener
  • WebSocket, socket.io, ws, mqttподожди это

отПриродаИз вышеперечисленного есть два вида:

  • на основеEventTargetподписка на событие

window, document, body, div и другие общие элементы, связанные с DOM, XMLHttpRequest,WebSocket,AudioContextПодождите, его суть в том, чтобы наследовать EventTarget.

  • Подписка на события на основе EventEmitter
    mqttа такжеwsосновывается наevents.
    известныйsocket.ioосновывается наcomponent-emitter.
    Их всех объединяет то, что черезonа такжеoffи другие методы для подписки и отмены событий.

Таким образом, нам становится проще отслеживать и собирать информацию о подписке на события и отказе от подписки.

Суть - прототип

не важно какEventTargetсерия илиEventEmitterСуть серии заключается в подписке и отмене подписки на экземпляр после его создания.

при создании экземпляра, чтобы большеДобавить хорошее повторное использование и меньше накладных расходов памяти, обычно помещайте общедоступные методы вprototypeВыше да, суть проблемы все сводится к прототипу.

Поэтому коллбэк событий делать на нативе, а методы подписки и отписки переписать на прототипе.Два момента:

  1. Собирать информацию о мониторинге событий
  2. сохранить исходную функцию

Дальнейшая суть - перехват метода, далее подойдем к перехвату метода вместе

перехват метода

Несколько методов перехвата

Перехват методов, я собрал и организовал здесь около 7 методов, конечно, некоторые из них похожи, и проблема не большая:

  • Просто переопределите исходный метод
  • простой прокси
  • Наследование
  • Динамический прокси
  • Стандартный прокси ES6+
  • Стандарт ES5 defineProperty
  • декотатор ES6 + стандарт

Простой пример каждого метода можно найти здесьНесколько методов перехвата

Идеальные и универсальные, конечно, последние три.

  • decotator
    Явно здесь не подходит.Во-первых, декоратору нужноявныйВо-вторых, слишком высокая цена.

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

  • Proxy

один: Прокси, который возвращает новый объект, вам нужно использовать этот новый объект для эффективного проксирования.
Второй: Мы несем ответственность за то, что делаем,Для поддержки восстановления, поэтому, если быть более точным, мы собираемся использовать отменяемый прокси. Простой код выглядит следующим образом:

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

Как собрать функции прослушивания событий DOM

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

сторонняя библиотекаgetEventListeners

Он просто напрямую модифицирует метод-прототип и хранит соответствующую информацию на узле Результат возможен, и так играть не рекомендуется.

недостаток

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

Метод getEventListeners консоли Chrome получает события одного узла. 

недостаток

  1. Можно использовать только в консоли
  2. Вы можете получать события прослушивателя только одного элемента за раз

хром консоль, Элементы => Прослушиватели событий

  1. Может использоваться только в интерфейсе инструмента разработчика
  2. Относительно проблематично найти

chrome дополнительные инструменты => Монитор производительности может получать прослушиватели событий JS, что является общим количеством событий

Подробной информации нет, только статистика.

Проблемы со структурами данных и слабыми ссылками

WeakRef уже упоминался ранее, но мы должны подумать о том, на какие объекты нужно слабо ссылаться.

Какую структуру данных выбрать

Так как это для статистики и анализа, некоторые данные должны быть сохранены.
И здесь нам нужно использовать объект в качестве ключа, потому что мы хотим подсчитать подписку на событие экземпляра EventTarget или EventEmitter.

Итак, предпочтения, Map, Set, WeakMap, WeakSet, кого бы вы выбрали?
WeapMap и WeakSet выглядят хорошо, но есть很致命проблема, то есть不能进行遍历. Если вы не можете пройти, конечно, вы не можете вести статистику.

Так что здесь правильнее выбрать Карту.

Слабые ссылки на эти данные

Во-первых, перечислите данные, которые необходимо разработать для подписки на события и отписки:

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

Анализ управляющего кода:

  1. target
    Объект, смонтированный событием, если это EventEmitter, то смонтированный объект является его экземпляром
  2. type
    тип события
  3. listener
    функция слушателя
  4. options
    Опции, хотя у EventEmitter их нет, мы не думаем, что это то же самое.

выбрать правильныйtargetа такжеlistenerделать слабые ссылки, , общая структура хранения данных выглядит следующим образом.

image.png

МыЗависимое от события тело и функция обратного вызова событияЧтобы сделать слабую ссылку, TypeScript выражает ее следующим образом:

interface EventsMapItem {
    listener: WeakRef<Function>;
    options: TypeListenerOptions
}

Map<WeakRef<Object>, Record<string, EventsMapItem[]>>

Вроде бы все ок, но на самом деле есть проблема, которую нельзя недооценивать.При поддержке программы количество ключей Map увеличится.Этот ключ WeakRef.Объекты, на которые слабо ссылается WeakRef, могут быть переработаны, но WeakRef, связанный с targetget, не будет переработан.

Конечно,你可以周期性的去清理,也可以遍历的时候无视这些没有真实引用的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);
    }
  )

Это нетрудно понять, потому что ключ КартыWeakRef<object>, поэтому после того, как цель будет переработана, нам нужно удалить связанную с ней 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Значение по умолчанию неверно.

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

еслиaddEventListenerВозвращается логическое значение, которое можно использовать в качестве основы для суждения.undefined, Провидение, смейся, не плачь.

В этот момент некоторые люди должны смеяться, разве это не повторяется? Так в чем утечка? ? ?

основной источник утечки

Я начал с того, что сказалSPA усугубляет это явление, это явление заключается в том, что функция события вызывает утечку памяти.

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

this.onResizeОн создается вместе с компонентом, поэтому каждый раз при создании компонента он тоже будет создаваться один раз.Хотя код тот же, но все же новый.когда компонент уничтожен, ноthis.onResizeНа него ссылается окно, и его нельзя уничтожить.

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

что такое та же функция

Как судить же функция стала нашим ключом.

Такая же ссылка есть конечно, в системе подписки на события на основе EventTarget она естественно экранирована, а системе подписки на основе EventEmitter не так повезло.

В это время все видят каждый день, но и игнорируют невидимый метод.toString, да, это он, это он, это он наш милый маленькийtoString.

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

Ты все еще помнишьЮрберриseajs, это зависит от поиска, то есть с помощьюtoString

image.png

Сравниваем контент, в подавляющем большинстве случаев проблем нет, кроме:

  1. Ваш код функции действительно такой же
    В ESLint есть правило, похоже, оно не используетсяthisМетод не должен быть написан в классе. Действительно же, следует подумать о реализации кода.
  2. встроенная функция
const random = Math.random
console.log("name:", random.name, ",content:", random.toString())
// name: random ,content: function random() { [native code] }
  1. связанная функция
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] }

Итак, мы проверяем встроенную функцию и функцию после привязки, основная идеяnameа также{ [native code] }.

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

переписать привязку

Ответ заключается в том, чтобы переписать 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. Перезапись привязки не включена по умолчанию
    Основное устранение неполадок почти завершено, затем включите параметр перезаписи привязки и анализируйте функцию прослушивателя событий только после ее привязки.

Как определить, является ли это функцией после привязки или вышеупомянутой

  1. имя функции, имя которойbound [原函数名]
  2. функциональное тело,{ [native code] }

резюме

Основная идея

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

два сомнения

  1. совместимость
    Да, его можно использовать только для отладки относительно новых браузеров. Впрочем, нет проблем!Найден и отремонтирован, а младшая версия имеет большую вероятность починки.

  2. Как отлаживать мобильный терминал
    Да, но не в центре внимания этой статьи.

После того, как анализ вышеуказанных проблем завершен, мы всем и только обязаны Dongfeng.

Следите за обновлениямиСтатьи кода

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

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