Устранение утечек памяти в приложении
В этой статье мы рассмотрим два типа: утечки по слушателям событий и по интервалам / тайм-аутам. Я буду использовать 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