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 для размещения комментария