LINUX.ORG.RU

JavaScript, циклы и генераторы

 


0

2

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

В современных языках программирования много средств для написания простого понятного выразительного кода. Сейчас попробовал на Джаваскрипте. А нихрена. Оказалось, далеко не все массивы умеют .forEach, forEach не сочетается с yield, setInterval не принимает генераторы и т. д. В итоге пришёл к синтаксису недо-Си — имитации цикла for через if с глобальными переменными:

    let pause = 1000;
    let length = 10;
    let i = 0;
    let doRoll = null;

    function getNext() {
        console.log(array1[i]);
        i++;
        if (i >= array1.length) {
            clearInterval(doRoll);
            doRoll = null;
        }
    }

    function startRolling() {
        if (doRoll) {
            clearInterval(doRoll);
            doRoll = null;
        } else {
            i = 0;
            doRoll = setInterval(getNext, pause);
        }
    }

Как это записать покрасивее?

★★★★★

далеко не все массивы умеют .forEach

Не помню, но это какие-то объекты браузера типа HTMLCollection и чё-то такое. Чтобы по ним делать .forEach делают либо Array.from(collection).forEach либо [...collection].forEach

forEach не сочетается с yield

В каком смысле?

setInterval не принимает генераторы

куда он должен их принимать?

Bad_ptr ★★★★★
()

А глобальные переменные лучше прятать в замыкания. Чтобы у каждого цикла были свои переменные, а не общие для всех.

Bad_ptr ★★★★★
()
Ответ на: комментарий от Bad_ptr

А глобальные переменные лучше прятать в замыкания. Чтобы у каждого цикла были свои переменные, а не общие для всех.

Можно пример? Как этот подход применить к doRoll? Она используется как флаг, запускался ли уже скрипт.

question4 ★★★★★
() автор топика
Ответ на: комментарий от Bad_ptr

это какие-то объекты браузера типа HTMLCollection

Именно. Но с другими объектами прокатывает.

forEach не сочетается с yield

В каком смысле?

Пытался yield поставить внутри forEach.

setInterval не принимает генераторы

куда он должен их принимать?

Например, setInterval(generator1.next, 10000)

question4 ★★★★★
() автор топика

глобальными переменными:

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

yield

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

.forEach

А как ты собрался forEach с таймером скрещивать?

firkax ★★★★★
()
Ответ на: комментарий от question4

Именно. Но с другими объектами прокатывает

Ну вот и просто в тех местах где хочешь использовать .forEach оберни переменную в Array.from(input) или […input]

Пытался yield поставить внутри forEach

Ну не внутри forEach, а внутри анонимной функции(не генератора), которую пытался передать в forEach

Например, setInterval(generator1.next, 10000)

А если setInterval(_ => generator1.next(), 1000) ?

Можно пример? Как этот подход применить к doRoll?

В яваскрипт функция может возвращать функцию, а функция захватывает использованные внутри функции переменные из окружения.
Например так:

function RollArray(array, pause = 1000, i = 0) {
	let doRoll = null;

	const getNext = _ => {
		console.log(array[i]);
		i++;
		if (i >= array.length) {
			clearInterval(doRoll);
			doRoll = null;
		}
	}
	doRoll = setInterval(getNext, pause);
	return {
		intervalId : () => doRoll,
		currentI : ()  => i,
		cancel : _ => if (doRoll) { clearInterval(doRoll); doRoll = null },
	};
}

Вообще конечно надо читать учебники по яваскрипту, по сишке, расту, лиспу. :)

Bad_ptr ★★★★★
()
let array = [1, 2, 3, 4, 5];
for (const item of array) {
  await new Promise(resolve => setTimeout(resolve, 1000));
  console.log(item);
}
static_lab ★★★★★
()
Ответ на: комментарий от firkax

yield

Это ужасный костыль, вообще не надо им пользоваться.

Чем можно? Чтобы каждый вызов обрабатывал следующий элемент списка.

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

Вот у меня есть таймер setInterval, который каждый раз вызывает одну и ту же функцию. Можно ли сделать, чтобы эта функция помнила своё предыдущее состояние и при каждом вызове переходила к следующему элементу?

А как ты собрался forEach с таймером скрещивать?

А как можно? Например, можно ли выполнить какое-то действие с текущим элементом и ждать PAUSE миллисекунд?

question4 ★★★★★
() автор топика

Как это записать покрасивее?

    function intervalRolling(arr, callback, pause, final_cb) {
      this.arr = arr, this.cb = callback, this.pause = pause, this.final_cb = final_cb, this.i = 0;
      this.iId = setInterval((function() {
        this.cb(this.arr[this.i]);
        this.i++;
        if(this.i >= this.arr.length) {
          clearInterval(this.iId); this.iId = null;
          if(this.final_cb) this.final_cb();
        }
      }).bind(this), pause);
    }
    intervalRolling.prototype.stop = function() {
      if(this.iId!=null) { clearInterval(this.iId); this.iId = null; }
    }

    // пример использования
    function getNext(item) {
      console.log(item);
    }
    array1 = [ 'a1', 'a2', 'a3' ];
    xxx = new intervalRolling(array1, getNext, 1000);
// или так:    xxx = new intervalRolling(array1, getNext, 1000, function() { console.log('done'); });

// отмена: xxx.stop();
firkax ★★★★★
()
Последнее исправление: firkax (всего исправлений: 1)
Ответ на: комментарий от question4

Чем можно? Чтобы каждый вызов обрабатывал следующий элемент списка.

Запоминай состояние в переменной. В моём примере выше, например, эта переменная расположена в объекте интервального цикла.

Можно ли сделать, чтобы эта функция помнила своё предыдущее состояние

Да, забинди её к объекту и храни состояние в нём, он доступен через this. Или другой вариант (он не такой красивый, но тоже можно):

function startRolling() {
  var i = 0;
  getNext = function () {
     ... /* тут доступна внешняя переменная i, и она не будет сбрасываться между вызовами */
  }
}
/* а тут она уже не доступна - не засоряем глобальное пространство имён */
Хотя в этом варианте, если все переменные сделать локальными к стартовой функции, не получится делать ручной стоп потом (следующий вызов стартовой функции будет уже новые экземеляры локальной переменной использовать).

А как можно? Например, можно ли выполнить какое-то действие с текущим элементом и ждать PAUSE миллисекунд?

Скорее всего никак. Ну я в предыдущем комменте привёл конструкцию, которая вполне эмулирует то что ты хочешь и вызывается в одну строчку.

firkax ★★★★★
()
Ответ на: комментарий от question4

Вот у меня есть таймер setInterval, который каждый раз вызывает одну и ту же функцию. Можно ли сделать, чтобы эта функция помнила своё предыдущее состояние и при каждом вызове переходила к следующему элементу?

Вот через итератор:

const array = [1, 2, 3, 4, 5];
const iterator = array[Symbol.iterator]();

const timer = setInterval(() => {
    const item = iterator.next();
    if (item.done) {
        clearInterval(timer);
    } else {
        console.log(item.value);
    }
}, 1000);
static_lab ★★★★★
()
Ответ на: комментарий от static_lab

Однако у тебя тоже глобальная переменная. Чтобы от неё красиво избавиться, всё равно придётся делать код примерно как мой. Ну разве что вместо i++, arr[i] писать iterator.next();

firkax ★★★★★
()
Последнее исправление: firkax (всего исправлений: 2)

Как в Джаваскрипте сделать вызов итератора по массиву через setInterval?

const enumerateAndPrint = <T>(xs: Iterable<T>, intervalMillis: number) => {
    const iterator = xs[Symbol.iterator]();
    const intervalId = setInterval(() => {
        const { value, done } = iterator.next();
        if (done) {
            clearInterval(intervalId);
            return;
        }
        console.log('array element: %o', value);
    }, intervalMillis);
};

enumerateAndPrint([1, 2, 3], 1000);
array element: 1
array element: 2
array element: 3

Можно заменить console.log на функцию, передаваемую дополнительным параметром.

Nervous ★★★★★
()
Последнее исправление: Nervous (всего исправлений: 1)
Ответ на: комментарий от firkax

Чтобы красиво избавиться от глобальной переменной, есть замыкания:

const array = [1, 2, 3, 4, 5];

function iterate(array, onEach, onDone) {
    const iterator = array[Symbol.iterator]();

    return () => {
        const item = iterator.next();
        if (item.done) {
            onDone();
        } else {
            onEach(item.value);
        }
    }
}


const timer = setInterval(iterate(
    array,
    (value) => console.log(value),
    () => clearInterval(timer)
), 1000);

Можно и без объекта итератора

function iterate(array, onEach, onDone) {
    let index = 0;

    return () => {
        if (index < array.length) {
            onDone();
        } else {
            onEach(array[index++]);
        }
    }
}
static_lab ★★★★★
()
Ответ на: комментарий от static_lab

Обрати внимание, что в начале темы автор среди прочего реализовал код для досрочной остановки «цикла», вероятно ему это нужно. С замыканием ты это сделать не сможешь, о чём я выше уже писал.

firkax ★★★★★
()
Ответ на: комментарий от firkax

С замыканием ты это сделать не сможешь

function iterate(array, onEach, onDone) {
    let index = 0;
    let isCanceled = false;

    return [
        () => {
            if (isCanceled || index >= array.length) {
                onDone();
            } else {
                onEach(array[index++]);
            }
        },
        () => {
            isCanceled = true;
        }
    ];
}

const [doTick, cancel] = iterate(
    array,
    (value) => console.log(value),
    () => clearInterval(timer)
);

const timer = setInterval(doTick, 1000);
static_lab ★★★★★
()
Последнее исправление: static_lab (всего исправлений: 2)
Ответ на: комментарий от firkax

код для досрочной остановки «цикла», вероятно ему это нужно. С замыканием ты это сделать не сможешь

Это можно сделать через AbortController или собственный подобный костыльобъект, который можно передать внутрь замыкания.

surefire ★★★
()
Ответ на: комментарий от surefire
function iterate(array, onEach, onDone, abortSignal) {
    let index = 0;

    return () => {
        if (abortSignal?.aborted || index >= array.length) {
            onDone();
        } else {
            onEach(array[index++]);
        }
    };
}

const controller = new AbortController();
const array = [1, 2, 3, 4, 5];

const timer = setInterval(iterate(
    array,
    (value) => console.log(value),
    () => clearInterval(timer),
    controller.signal
), 1000);

setTimeout(() => {
    controller.abort();
}, 2500);
static_lab ★★★★★
()
Ответ на: комментарий от static_lab

Ладно, смог. Но с объектом всё равно лучше смотрится, к тому же ты по сути почти его и сделал, только с безымянными полями - элементами массива.

firkax ★★★★★
()
Ответ на: комментарий от firkax

Но с объектом всё равно лучше смотрится

Это вкусовщина, конечно, но в самой функции появляется куча this, которые засоряют код.

static_lab ★★★★★
()

далеко не все массивы умеют .forEach

Все массивы умеют. Не умеют объекты, которые выдают себя за массив. Зато все они умеют for..of. Это ещё и быстрее.

forEach не сочетается с yield

Ясен хрен, ведь в forEach нужно передать функцию, а внутри функции yield не имеет смысла (внезапно)

setInterval не принимает генераторы

Зато принимает функции, заворачиваешь () => { ... const i = gen.next() ... } и всё.

no-such-file ★★★★★
()
Последнее исправление: no-such-file (всего исправлений: 2)
Ответ на: комментарий от LightDiver

Он научился, чтобы помочь в этом треде. Вот серьезно, такие язычки за вечер можно изучить, если есть база. Но вот каждый день писать такую дрисню не очень хочется.

bread
()

Ещё один вариант без глобального состояния

const delay = async (timeoutMillis: number) =>
    new Promise<void>((resolve) => {
        setTimeout(() => {
            resolve();
        }, timeoutMillis);
    });

const enumerateWithDelay = async <T>(
    xs: Iterable<T>,
    fn: (value: T) => void,
    intervalMillis: number,
) => {
    for (const x of xs) {
        await delay(intervalMillis);
        fn(x);
    }
};

await enumerateWithDelay([3, 4, 5], console.log, 1000);
1 
2 
3

А какую задачу решаем-то?

Nervous ★★★★★
()
Ответ на: комментарий от question4

Черновик ответа остался? :)

Если считаешь, что оно действительно заслуживает ответа и полемики – я восстановлю. Просто я счёл, что это был комментарий из серии «скормила вопрос ИИ и тут же без проверки выдала в форум». Что мог бы сделать и ТС, если бы его действительно интересовала такая информация. Критерий – специфический стиль изложения.

hobbit ★★★★★
()
Последнее исправление: hobbit (всего исправлений: 1)
Ответ на: комментарий от static_lab

А я ещё один способ придумал - можно ж вернуть из замыкания просто функцию остановки. Тоже будет без объекта.

    function intervalRolling(arr, callback, pause, final_cb) {
      var i = 0, iId;
      iId = setInterval(function() {
        callback(arr[i]);
        i++;
        if(i >= arr.length) {
          clearInterval(iId); iId = null;
          if(final_cb) final_cb();
        }
      }, pause);
      return function () {
        if(iId!=null) { clearInterval(iId); iId = null; }
      }
    }

    // пример использования
    function getNext(item) {
      console.log(item);
    }
    array1 = [ 'a1', 'a2', 'a3' ];
    stopFunc = intervalRolling(array1, getNext, 1000);
// отмена: stopFunc();
Но это тут прокатывает только потому что функция всего одна, иначе всё равно если не объект, так что-то на него похожее создавать придётся. Да и тут, хоть код реализации и получился короче, но код использования выглядит менее наглядным: вызвать метод stop() у объекта это очевидное действие, а вот интерпретировать безымянный возврат из intervalRolling() как стоп-функцию - уже не так очевидно.

firkax ★★★★★
()
function* generator(arr) {
  for (let item of arr) {
    yield item;
  }
}

const g = generator([1, 2, 3]);

function callback() {
  const result = g.next();
  if (!result.done) {
    console.log(result.value);
    setTimeout(callback, 1000);
  } else {
    console.log("done");
  }
}

setTimeout(callback, 1000);
vbr ★★★★★
()
Ответ на: комментарий от hobbit

Черновик ответа остался? :)

Нет.

«скормила вопрос ИИ и тут же без проверки выдала в форум»

Пожалуй, отсутствие проверки отвечает на возникшие у меня вопросы.

Если считаешь, что оно действительно заслуживает ответа и полемики

Ты уже ответил.

Что мог бы сделать и ТС, если бы его действительно интересовала такая информация.

Спрашивал :) Часть ответов включил в код, часть не понравились. Поэтому спрашиваю живых людей.

Критерий – специфический стиль изложения.

Понятно. Точнее, совсем непонятно, что с ним не так, но доверюсь опыту модератора.

question4 ★★★★★
() автор топика

в продолжение специальной олимпиады:


async function* getSome(xs, delay) {
    const f = (x) => new Promise((resolve) => setTimeout(() => resolve(x), delay));
    for await (const x of xs) {
        yield f(x);
    }
}

const xs = [1,2,3,4];

for await (const x of getSome(xs, 100)) { console.log(`x: ${x}`); }
for await (const x of getSome(getSome(xs, 100), 200)) { console.log(`x: ${x}`); }

drsm ★★
()
Последнее исправление: drsm (всего исправлений: 1)
Ответ на: комментарий от firkax

интерпретировать безымянный возврат из intervalRolling() как стоп-функцию - уже не так очевидно

Почему? Это типичная функция отмены/отписки. Такой паттерн часто встречается в JS.

Ещё если функция-конструктор возвращает функцию, которую можно передавать во внешний setInterval, то дополнительные функции можно прилинковать и к ней — по сути уже готовым объектом функции пользуемся.

static_lab ★★★★★
()
Ответ на: комментарий от static_lab

Ну ладно, может быть. Но у меня представления о красивом коде не из js формировались.

firkax ★★★★★
()
  • Markdown
Пустая строка (два раза Enter) начинает новый абзац. Знак '>' в начале абзаца выделяет абзац курсивом цитирования.
Внимание: прочитайте описание разметки Markdown.
Используйте Ctrl-Enter для размещения комментария