Устранение утечек памяти в приложении

В этой статье мы рассмотрим два типа: утечки по слушателям событий и по интервалам / тайм-аутам. Я буду использовать React в качестве примера, но основные концепции применимы к любой платформе, которая занимается добавлением и удалением узлов DOM на странице.

Базовое приложение

Содержание статьи:

Допустим, у вас есть базовое приложение, которое просто добавляет и удаляет дочерние компоненты, в данном случае компонент под названием Snoopy:

JavaScript class App extends React.Component { constructor() { super(); this.state = { snoopy: null, }; } render() { return ( <div> <h1>This is an app</h1> {this.state.snoopy ? ( <div> <Snoopy /> <button onClick={() => this.setState({snoopy: false})}> Remove the Snoopy component </button> </div> ) : ( <button onClick={() => this.setState({snoopy: true})}> Add a Snoopy component </button> )} </div> ); } }

12345678910111213141516171819202122232425262728 class App extends React.Component {  constructor() {    super();    this.state = {      snoopy: null,    };  }   render() {    return (      <div>        <h1>This is an app</h1>        {this.state.snoopy ? (          <div>            <Snoopy />            <button onClick={() => this.setState({snoopy: false})}>              Remove the Snoopy component            </button>          </div>        ) : (          <button onClick={() => this.setState({snoopy: true})}>            Add a Snoopy component          </button>        )}      </div>    );  }}

React JS. Основы

Изучите основы ReactJS на практическом примере по созданию учебного веб-приложения

Получить курс сейчас!

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

Snoopy

А что делает Snoopy? Он отслеживает нажатия клавиш.

JavaScript class Snoopy extends React.Component { constructor() { super(); this.state = { keys: [], }; } componentDidMount() { document.addEventListener(‘keydown’, (e) => { const keys = […this.state.keys]; keys.push(e.keyCode); console.log(e.keyCode); }); } render() { return ( <p> I am Snoopy. I have been snooping your keystrokes. <br />I am delighted in inform you that your console has a list of the key codes you pressed </p> ); } }

123456789101112131415161718192021222324 class Snoopy extends React.Component {  constructor() {    super();    this.state = {      keys: [],    };  }  componentDidMount() {    document.addEventListener(‘keydown’, (e) => {      const keys = […this.state.keys];      keys.push(e.keyCode);      console.log(e.keyCode);    });  }  render() {    return (      <p>        I am Snoopy. I have been snooping your keystrokes.        <br />I am delighted in inform you that your console has a list of        the key codes you pressed      </p>    );  }}

Ничего особенного, правда? Но что происходит после удаления Snoopy из приложения? Он отслеживает нажатия клавиш. Несмотря на то, что Snoopy отсутствует в приложении и в дереве DOM нет его следов, функция слушателя все еще находится в памяти и все еще… отслеживает.

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

Это был пример того, что слушатель событий DOM является unchecked. Вы только что вызвали утечку памяти. И не было никаких указаний на то, что что-то пошло не так, ни ошибки, ни даже предупреждения в консоли. Что, если Snoopy также включает временной интервал?

JavaScript class Snoopy extends React.Component { constructor() { super(); this.state = { seconds: 0, keys: [], }; } componentDidMount() { // task 1 const i = setInterval(() => { const seconds = this.state.seconds + 1; this.setState({seconds}); console.info(seconds); }, 1000); // task 2 document.addEventListener(‘keydown’, (e) => { const keys = […this.state.keys]; keys.push(e.keyCode); this.setState({keys}); console.log(e.keyCode); }); } render() { return ( <p> I am Snoopy. I have been snooping your keystrokes for{‘ ‘} {this.state.seconds} seconds. <br />I am delighted in inform you that so far you have pressed keys with the following codes: <br /> {this.state.keys.join(‘, ‘)} </p> ); } }

123456789101112131415161718192021222324252627282930313233343536 class Snoopy extends React.Component {  constructor() {    super();    this.state = {      seconds: 0,      keys: [],    };  }  componentDidMount() {    // task 1    const i = setInterval(() => {      const seconds = this.state.seconds + 1;      this.setState({seconds});      console.info(seconds);    }, 1000);    // task 2    document.addEventListener(‘keydown’, (e) => {      const keys = […this.state.keys];      keys.push(e.keyCode);      this.setState({keys});      console.log(e.keyCode);    });  }  render() {    return (      <p>        I am Snoopy. I have been snooping your keystrokes for{‘ ‘}        {this.state.seconds} seconds.        <br />I am delighted in inform you that so far you have pressed        keys with the following codes:        <br />        {this.state.keys.join(‘, ‘)}      </p>    );  }}

Теперь, когда Snoopy удаляется из DOM, setInterval продолжает кликать и делать что-то. Функция, вызываемая setInterval, все еще жива и работает в памяти.

Этот пример, тем не менее, касается состояния в обеих функциях (обратите внимание на вызовы this.setState()). В результате, когда Snoopy удаляется из DOM (отключен), React выдаст предупреждение в консоли. И, надеюсь, разработчик обратит внимание на эти предупреждения, поскольку они отображаются только в процессе разработки, а не в производственной среде. Таким образом, это предупреждение можно легко пропустить в достаточно сложном приложении, которое много печатает в консоли. И это при условии, что предупреждение даже появляется, потому что помните, если функции с утечкой памяти не касаются состояния, предупреждений не будет.

Устранение утечек

Решение для этих утечек аналогичное, оно включает в себя уборку за собой. Если вы добавляете EventListener, вы должны удалить EventListener. Если вы устанавливаете Interval / setTimeout, вы должны удалить Interval / clearTimeout.

React JS. Основы

Изучите основы ReactJS на практическом примере по созданию учебного веб-приложения

Получить курс сейчас!

В случае компонентов React это означает использование метода жизненного цикла componentWillUnmount(). Для этого также потребуется, чтобы слушатель событий больше не был встроенной функцией. Также потребуется, чтобы идентификатор интервала хранился где-нибудь, откуда его можно было бы извлечь, для очистки. Что-то вроде этого:

JavaScript class Snoopy extends React.Component { constructor() { super(); this.state = { seconds: 0, keys: [], }; this.keydownHandler = this.keydownHandler.bind(this); this.intervalID = null; } keydownHandler(e) { const keys = […this.state.keys]; keys.push(e.keyCode); this.setState({keys}); console.log(e.keyCode); } componentDidMount() { // task 1 this.intervalID = setInterval(() => { const seconds = this.state.seconds + 1; this.setState({seconds}); console.info(seconds); }, 1000); // task 2 document.addEventListener(‘keydown’, this.keydownHandler); } componentWillUnmount() { // task 1 cleanup clearInterval(this.intervalID); // task 2 cleanup document.removeEventListener(‘keydown’, this.keydownHandler); } render() { return ( <p> I am Snoopy. I have been snooping your keystrokes for{‘ ‘} {this.state.seconds} seconds. <br />I am delighted in inform you that so far you have pressed keys with the following codes: <br /> {this.state.keys.join(‘, ‘)} </p> ); } }

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748 class Snoopy extends React.Component {  constructor() {    super();    this.state = {      seconds: 0,      keys: [],    };    this.keydownHandler = this.keydownHandler.bind(this);    this.intervalID = null;  }   keydownHandler(e) {    const keys = […this.state.keys];    keys.push(e.keyCode);    this.setState({keys});    console.log(e.keyCode);  }   componentDidMount() {    // task 1    this.intervalID = setInterval(() => {      const seconds = this.state.seconds + 1;      this.setState({seconds});      console.info(seconds);    }, 1000);    // task 2    document.addEventListener(‘keydown’, this.keydownHandler);  }   componentWillUnmount() {    // task 1 cleanup    clearInterval(this.intervalID);    // task 2 cleanup    document.removeEventListener(‘keydown’, this.keydownHandler);  }  render() {    return (      <p>        I am Snoopy. I have been snooping your keystrokes for{‘ ‘}        {this.state.seconds} seconds.        <br />I am delighted in inform you that so far you have pressed        keys with the following codes:        <br />        {this.state.keys.join(‘, ‘)}      </p>    );  }}

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

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

То же, но с хуками

При использовании хуков вам понадобится useEffect () для настройки таких «эффектных» задач, а также для устранения беспорядка, создаваемого ими. Слово «эффект» означает, что это побочные эффекты отрисовки компонента. Основная задача компонента — показать что-то на экране. Отслеживание времени и отслеживание кликов — побочные эффекты основной задачи. Но я отвлекся. Паттерн выглядит вот так:

JavaScript useEffect(() => { // set stuff up, like `componentDidMount()` return () => { // clean things up, like `componentWillUnmount()` }; }, []);

123456 useEffect(() => {  // set stuff up, like `componentDidMount()`  return () => {    // clean things up, like `componentWillUnmount()`  };}, []);

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

JavaScript function Snoopy() { const [seconds, setSeconds] = useState(0); const [keys, setKeys] = useState([]); // task 1 useEffect(() => { const intervalID = setInterval(() => { setSeconds(seconds + 1); console.info(seconds); }, 1000); return () => { clearInterval(intervalID); }; }, [seconds, setSeconds]); // task 2 useEffect(() => { function keydownHandler(e) { const newkeys = […keys]; newkeys.push(e.keyCode); setKeys(newkeys); console.log(e.keyCode); } document.addEventListener(‘keydown’, keydownHandler); return () => { document.removeEventListener(‘keydown’, keydownHandler); }; }, [keys, setKeys]); return ( <p> I am Snoopy. I have been snooping your keystrokes for {seconds}{‘ ‘} seconds. <br />I am delighted in inform you that so far you have pressed keys with the following codes: <br /> {keys.join(‘, ‘)} </p> ); }

12345678910111213141516171819202122232425262728293031323334353637383940 function Snoopy() {  const [seconds, setSeconds] = useState(0);  const [keys, setKeys] = useState([]);   // task 1  useEffect(() => {    const intervalID = setInterval(() => {      setSeconds(seconds + 1);      console.info(seconds);    }, 1000);    return () => {      clearInterval(intervalID);    };  }, [seconds, setSeconds]);   // task 2  useEffect(() => {    function keydownHandler(e) {      const newkeys = […keys];      newkeys.push(e.keyCode);      setKeys(newkeys);      console.log(e.keyCode);    }    document.addEventListener(‘keydown’, keydownHandler);    return () => {      document.removeEventListener(‘keydown’, keydownHandler);    };  }, [keys, setKeys]);   return (    <p>      I am Snoopy. I have been snooping your keystrokes for {seconds}{‘ ‘}      seconds.      <br />I am delighted in inform you that so far you have pressed keys      with the following codes:      <br />      {keys.join(‘, ‘)}    </p>  );}

Хорошие новости с точки зрения читаемости кода заключаются в том, что несвязанные задачи могут существовать в своих собственных мирах (также известные как вызовы useEffect ()), а также то, что код установки и удаления находится рядом друг с другом.

Заключение

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

Еще один совет: если у вас есть приложение React с компонентами класса, проверьте написание вашего componentWillUnmount. Убедитесь, что это не componentWillUnMount. Потому что, если метод написан с ошибкой, предупреждения не будет, и весь код очистки будет просто мертвым грузом. И, поверьте, это может случится с каждым. Так что проверьте это прямо сейчас!

Автор: Stoyan Stefanov

Источник: webformyself.com

Comments (0)
Add Comment