Управление памятью: обычные функции JavaScript против функций-генераторов

Едва ли не каждый программист сталкивается с проблемой перебора большого количества элементов (коллекции статей, изображения, записи в базе данных и т.д.). И всё в порядке, пока наш сервер или браузер не заявляет: “Эй, да что ты творишь-то? Не слишком ли много работы ты мне даёшь?” =) Тут приходится засучить рукава и немножко покопаться в коде.

Сперва разберёмся, что такое генератор в JavaScript. Это функция, которая может вернуть значение и затем продолжить выполнение функции. В то время как обычная функция JavaScript использует оператор return, функция-генератор использует оператор yield. Вот вам пример (обратите внимание на звёздочку перед именем функции):

Функция-генератор, которая возвращает все int числа от 0 до N

Если вызвать функцию с любым аргументом, она вернёт итератор, а не значение (как можно было ожидать).

Выполнение функции-генератора, вернувшего объект-итератор, а не значение

Для получения значения вызываем метод next() объекта-итератора.

Получение значения из итератора

Здесь текущий результат хранится в свойстве value возвращаемого объекта. А свойство done показывает, закончила ли свою работу функция-генератор.

В приведённом выше примере функции мы указали 4 в качестве аргумента для нашей функции-генератора, поэтому функция будет возвращать значения от 0 до 4 (включительно) на каждый вызов метода next().

Выполнение

Теперь сравним функцию-генератор и обычную функцию в операторе цикла for на большом числе элементов данных.

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

Функция, которая генерирует некое количество случайных чисел и возвращает их

Проделывая то же с функцией-генератором JavaScript, используем следующий код:

Функция-генератор JavaScript для создания некоторого количества случайных чисел

Протестируем обе функции, создав функцию main(), которая проверит: как изменилось использование памяти после перебора элементов.

Основная функция, которая использует `functionToExec` в цикле `for` для перебора случайных элементов и показывает разницу в использовании памяти до и после вызова функции

Теперь посмотрим на использование памяти с помощью простой функции, которая задействует свойство performance объекта window:

Функция использования памяти браузером в JavaScript в килобайтах

Вызываем метод main() с обычной функцией и функцией-генератором и подсчитываем объем используемой памяти с каждой из них.

Сперва выполняем стандартную функцию JavaScript main(bigAmountOfItems, 2000000).

Разница в памяти после перебора 2 млрд. элементов с обычной функцией

Затем выполняем функцию-генератор main(bigAmountOfItemsGenerator, 2000000).

Разница в памяти после перебора 2 млрд. элементов с функцией-генератором

Здесь стандартная функция JavaScript показывает увеличение памяти ~46.5 kilobytes против лишь ~0.125 kilobytes с генератором. Происходит это потому, что с функцией-генератором не нужно хранить в оперативной памяти все 2000000 элементов. Итератор позволяет отслеживать элемент текущей итерации и продолжать возвращать следующий до конца.

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

Кроме того, браузеры работают по генераторам с async / await. Но это уже тема другой статьи 🙂 Надеюсь, данная статья была для вас полезной. Спасибо за внимание 😉

Читайте также:


Перевод статьи Evgeny Melnikov: Use JavaScript Generator Functions to Reduce Memory Utilization

Предыдущая статьяКак писать Bash-однострочники для клонирования и управления GitHub/GitLab репозиториями
Следующая статьяУлучшение производительности .NET Core API