lib-loop
v0.1.3
Published
Simple node.js library for work with async loops
Maintainers
Readme
Простая нативная работа с циклами.
Библиотка javascript/typescript (ES6) для node.js.
- бесконечный цикл (loop) с возможностью выхода,
- управляемый цикл (next),
- игровой цикл (game loop),
- асинхронные циклы,
- асинхронная пауза.
Установка
npm install lib-loopили
yarn add lib-loopНачало работы
Можно импортировать методы как с отдельные функции:
import { loop, wait } from 'lib-loop';
loop...
wait...или вызывать их как статические методы объекта:
import * as loop from 'lib-loop';
loop.loop...
loop.wait...Список методов
async iterate(callback: () => void, maxIterations, milliseconds = 0): void**
Запускает цикл.
В цикле вызывается функция callback.
Второй аргумент задает количество итераций. Если задать ноль или отрицательное число, то цикл не запустится.
Третьим необязательным аргументом можно передать задержку между итерациями.
async loop(callback: (deltaTime: number) => boolean, milliseconds = 0, tps = 0): void**
Запускает бесконечный цикл.
В цикле вызывается функция callback.
Функция callback принимает необязательный аргумент:
- deltaTime - время в секундах, прошедшее с момента последнего обновления итерации цикла,
Важно понимать, что для первой итерации значение deltaTime нельзя учитывать, т.к. оно будет приближено или равно нулю.
Идея передачи deltaTime заключается в том, чтобы сделать условия внутри цикла независимыми от того, с какой частотой обновляется цикл.
Цикл работает, пока функция callback возвращает true.
Цикл завершится, когда функция callback вернет false.
После определения callback, функция цикла может принять два необязательных аргумента:
milliseconds - принудительная задержка между итерациями в миллисекундах,
tps - число итераций (тиков) в секунду.
Если указать tps, то в итерациях будут срабатывать автоматические паузы.
async frameLoop(callback: (deltaTime: number) => boolean): void
Бесконечный цикл, использует оптимизацию браузера через requestAnimationFrame:
синхронизацию с частотой обновления экрана,
экономию ресурсов в неактивном состоянии.
Метод requestAnimationFrame является распространенным решением в игровых движках.
Данный цикл не получится запускать в терминале, но его можно использовать для реализации игрового цикла.
async frameRate(milliseconds = 1000): number
Служебная функция, которая считает среднее количество кадров в секунду на устройстве и возвращает среднюю длительность одного кадра в миллисекундах.
Аргумент milliseconds задает время, в течение которого идет тест.
Чем больше время, тем точнее расчет.
По-умолчанию установлено значение 100. Это минимально рекомендуемое значение. Оно не будет точным и даст приблизительную оценку с некоторой погрешностью. Но при том оно не занимает много времени.
async wait(milliseconds: number): void**
Пауза для асинхронного кода.
Класс управляемого цикла
constructor(main: (this) => void): void
Создает цикл.
В цикле задается функция main, которая будет вызываться в каждой итерации.
В аргумент функции main передается ссылка на текущий экземпляр управляемого цикла. Таким образом, можно управлять циклом из функции main.
context: any
Свойство, хранящее любые пользовательские состояния цикла.
По-умолчанию - пустой объект.
iterate: number
Свойство, хранящее порядковый номер итерации, начиная с 0.
Значение -1 показывает, что ни одна итерация еще не была запущена.
async start(callback: (this) => void): void**
Запускает цикл.
Перед запуском цикла вызывается функция callback.
По-умолчанию callback задан как пустая функция.
async stop(callback: (this) => void): void**
Останавливает цикл.
После остановки вызывается функция callback.
По-умолчанию callback задан как пустая функция.
async next(callback: (this) => void): void**
Продолжает цикл, переходит к следующей итерации.
Перед запуском вызывается функция callback.
По-умолчанию callback задан как пустая функция.
async wait(milliseconds: number): void**
Алиас метода wait, который можно вызвать как метод класса.
Примеры
Бесконечный цикл
Предположим, у нас есть асинхронная функция main:
async function main() {
if (...) {
return false;
}
return true;
}Запускать эту функцию в цикле можно самым простым способом, даже прямо из тела индексного файла:
loop(main);Эквивалентный способ:
(async () => loop(main))();Если функция main синхронная:
function main(): boolean {
if (...) {
return false;
}
return true;
}Ее нужно запускать внутри асинхронного вызова:
loop(async () => main());Допускается запускать внутри синхронный вызов:
loop(() => {
...
return true;
});или синхронную функцию:
loop(main);Несколько циклов
Т.к. данный цикл асинхронный, то несколько циклов подряд будут запускаться параллельно.
Если вы хотите запускать их последовательно, вам нужно вызывать их через await:
await loop(...);
await loop(...);или:
(async () => {
await loop(...);
await loop(...);
})();Обработка ошибок
Обработку ошибок можно сделать так:
loop(() => main().catch((e) => {
console.error(e);
return false;
}));Цикл с заданным количеством итераций
Возьмем пример из бесконечного цикла и сделаем цикл с заданным количеством итераций, например 10:
async function main() {
...
}
iterate(main, 10);Пример управляемого цикла
Создадим простой управляемый цикл.
Определим функцию, которая будет выполняться в каждой итерации:
async function main(self: Infinite) {
console.log(self.iterate);
if (self.iterate >= 9) {
self.stop();
}
self.next();
}Далее создадим цикл и запустим его:
const infinite = new Infinite(main);
infinite.start();Добавим паузу в 1 секунду между итерациями и вывод в консоль служебных сообщений:
async function main(self: Infinite) {
console.log(self.iterate);
if (self.iterate >= 9) {
self.stop(() => {
console.log('Цикл остановлен')
});
}
await self.wait(1000);
self.next();
}
const infinite = new Infinite(main);
infinite.start(() => {
console.log('Цикл запущен')
});Создадим более сложный управляемый цикл. Здесь будет запуск, пауза, возобоновление и полная остановка.
async function main(self: Infinite) {
const { iterate, context } = self;
console.log(iterate);
if (iterate >= 9 && !context.paused) {
self.stop(async () => {
context.paused = true;
console.log('Цикл временно приостановлен');
await self.wait(1000);
self.start(() => {
console.log('Цикл продолжен');
});
});
}
if (iterate >= 19 && !context.stopped) {
self.stop(() => {
context.stopped = true;
console.log('Цикл завершен');
});
}
await self.wait(100);
self.next();
};
const infinite = new Infinite(main);
infinite.context.paused = false;
infinite.context.stopped = false;
infinite.start(() => {
console.log('Цикл запущен');
});Задержка между итерациями
Самый простой и правильный способ - задать время задержки вторым аргументом:
loop(main, 1000)Можно также добавлять задержки внутри кода исполняемой логики, например в функции main:
async function main() {
if (...) {
return false;
}
await wait(1000);
return true;
}Пауза в цикле
Если вам нужно временно приостановить выполнение команд внутри цикла, самым простым и правильным способом будет пропуск вызова этих команд по какому-либо условию.
Вот пример с активацией флага paused:
const paused = true;
loop(() => {
if (paused) {
return;
}
main();
})Можно также перенести эту логику внутрь вызываемой функции main:
const paused = true;
const main = async () => {
if (paused) {
return true;
}
...
}Игровой цикл
В этом примере мы используем цикл frameLoop в качестве игрового цикла.
Создадим игровой объект, который будет перемещаться по координате x со скоростью speed пикселей в секунду.
class GameObject {
x: number;
speed: number;
constructor() {
this.x = 0;
this.speed = 100;
}
}Зададим для этого класса метод update, который будет вызываться в каждом цикле.
Первым аргументом он принимает deltaTime - смещение по времени в секундах между текущей и предыдущей итерацией.
Его мы будем использовать для расчета и корректировки скорости. Мы также посчитаем fps и выведем в консоль.
update(deltaTime: number) {
this.x += this.speed * deltaTime;
const fps = 1 / deltaTime;
console.log(fps);
}Запустим наш цикл.
const gameObject = new GameObject();
frameLoop(gameObject.update);Когда мы умножаем скорость speed на deltaTime, мы получаем количество пикселей, на которое должен переместиться объект за этот кадр.
Мы знаем, что frameLoop привязан к частоте кадров fps.
Например, при fps 60 кадров в секунду deltaTime составит примерно 0.0166. За один цикл tick объект переместится на 200 * 0.0166 = 3.32 пикселя. Для 1 секунды (60 кадров) это составит 3.32 * 60 = 199.2 пикселей.
Если же fps будет, например, 120 кадров в секунду, то deltaTime составит примерно 0.0083. За один цикл tick объект переместится на 200 * 0.0083 = 1.66 пикселя. Для 1 секунды (120 кадров) это составит 1.66 * 120 = 199.2 пикселя.
Если же вдруг fps снизится, например, до 30 кадров в секунду, то deltaTime составит примерно 0.0333. За один цикл tick объект переместится на 200 * 0.0333 = 6.66 пикселя. Для 1 секунды (120 кадров) это составит 6.66 * 30 = 199.8 пикселей.
В результате, независимо от частоты кадров, объект будет перемещаться с одинаковой скоростью.
Без deltaTime скорость перемещения будет сильно меняться в зависимости от часты кадров.
Версии
0.1.2
Теперь frameLoop поддерживает браузерный метод visibilitychange, который отслеживает, когда вкладка или окно меняет видимость.
При потере видимости, frameLoop полностью прерывает выполнение цикла, но при восстановлении видимости, запускает цикл заново.
Это позволяет значительно сэкономить ресурсы в фоновом режиме.
0.1.1
Из метода frameLoop удален необязательный параметр milliseconds. Использование задержки здесь не имеет смысла, потому что цикл привязан к браузерному методу обновления частоты кадров.
Лишний функционал создает пусть неощутимую, но тем не менее небольшую нагрузку.
Если вы хотите выбрать цикл с гибкими настройками - используйте loop.
Лицензия
Лицензия MIT, 2025
