Почему установлен антипаттерн и как его избежать

После того, как я написал React какое-то время, был один раз (и много раз) я запустил свой код и получил следующее предупреждение:

Warning: Can only update a mounted or mounting component. This usually means you called setState, replaceState, or forceUpdate on an unmounted component. This is a no-op.

Если вы пишете React, то, вероятно, вам знакомо это предупреждение. В основном это означает, что setState вызывается после componentWillUnmount, поэтому компонент больше не монтируется, так зачем вам состояние? но это также означает, что после того, как компонент был размонтирован, его память не была освобождена. При программировании, когда память должна освободиться, но этого не происходит, - это утечка памяти. То, что утечка памяти не гарантируется, но может быть и поздним выпуском памяти.

Вот пример кода, демонстрирующий проблему:

Давайте проигнорируем фальшивый метод рендеринга.
Метод извлечения данных является асинхронным, await API.getSomeData() может разрешать или отклонять после того, как компонент был уже размонтирован, и это закончится Warning: Can only update a mounted or mounting component...

Как каждый разработчик, я спросил Google, что делать, и быстро получил следующую принятую концепцию:
«на componentDidMount установлен this.isMounted=true и на componentWillUnmount установлен this.isMounted=false. А теперь, прежде чем звонить this.setState, просто сделайте волшебство if(this.isMounted) { this.setState({...})} ». - Предупреждений больше нет, миссия выполнена!

В тот момент я понял, что принятый ответ на переполнение стека не означает, что это хороший ответ. Я также был немного напуган тем, что этот (анти) шаблон является общепринятым ответом во многих сообщениях. React предлагает множество функций. Вы когда-нибудь спрашивали себя: Почему разработчики React не добавили смонтированный API? (Они действительно сделали, но они также удалили его. Они также описали здесь, почему это антипаттерн и почему он был удален)

Приведем пример кода, который продемонстрирует проблему с шаблоном isMounted.

Он определенно удалил предупреждение, и теперь у меня нет никаких предупреждений, так что мой код идеален! К сожалению, он далек от совершенства, он даже плох и имеет утечку памяти. Этот экземпляр MyComponent памяти никогда не будет свободным, потому что есть закрытие (обратный вызов setInterval), указывающее на this, поэтому this все еще жив, представьте себе создание 10 тысяч экземпляров этого компонента, это много памяти!

Очевидное решение для этого - сделать недействительным интервал, когда вызывается componentWillUnmount:

Теперь мы решили две проблемы: мы больше не получаем предупреждение и больше не имеем утечки памяти.

Каждую секунду память компонента не освобождается после размонтирования, не нужна секунда

Мир больше, чем setInterval, Когда запускается сборщик мусора, мы хотим, чтобы он очищал как можно больше.
Для этого мы должны были убедиться, что после вызова componentWillUnmount каждый ресурс async становится недействительным.

Вот список вещей, которые нам пришлось признать недействительными:

  1. Обещания
  2. Таймеры (интервал, тайм-аут)
  3. Слушатели DOM (документ и окно)
  4. Слушатели эмиттера событий (мы используем fbemitter)

Вот как в конечном итоге будет выглядеть наш код:

Все немедленно отменяется после вызова componentWillUnmount, но мы считаем, что это должно быть более коротким и общим

Отмена обещания
На самом деле вы не можете отменить обещание (если не используете bluebird). На языке JavaScript с этим есть некоторые проблемы. Я не буду здесь писать, как можно отменить обещание, но для этого есть несколько закономерностей. По сути, это просто немедленный отказ от них, не дожидаясь первоначального обещания.

Вернемся к componentWillUnmount с большим количеством строк кода

Вы можете догадаться, что мы сделали?

Мы собрали всех слушателей в одном месте, которое обрабатывает их всех и при необходимости делает недействительными.

Отменяемые события браузера (NPM)
https://www.npmjs.com/package/browser-cancelable-events
https://github.com/KromDaniel / browser-cancelable-events

Отменяемые события браузера - это небольшой проект, который я начал более чистым и универсальным способом, чтобы справиться с отключением недействительности, просто создайте экземпляр CancelableEvents после инициализации компонента, а на componentWillUnmount просто отмените все с помощью метода cancelAll.
Теперь добавление новых слушателей будет зарегистрировано в этом экземпляре, и код выглядит намного чище.

Некоторое объяснение кода:

  1. Вызывается constructor, инициализируем this.cancelable = new CancelableEvents(); на componentWillUnmount добавляем this.cancelable.cancelAll();
  2. регистрация отменяется для наблюдения за событием окна resize
  3. updateEverySecond регистрирует интервал, который обновляет состояние каждую секунду
  4. fetchData асинхронный метод, вызывающий API

Теперь, когда вызывается componentWillUnmount, мы мгновенно аннулируем всех слушателей в одном месте.

Подведем итог:

  1. Избегайте использования this.isMounted, это антипаттерн.
  2. Когда вы видите принятый ответ на переполнение стека, это не значит, что это хороший ответ, не верьте сразу всему, что вы видите в Интернете (даже не этому сообщению)
  3. Получить утечки памяти легко, время от времени используйте инструмент отладчика и сделайте снимок кучи. Убедитесь, что память вашего кода освобождается.
  4. Существует множество решений и шаблонов того, как сделать асинхронные задачи недействительными, вы можете использовать browser-cancelable-events, но я уверен, что есть много других замечательных инструментов.

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