Генераторы Javascript с (а)синхронной функциональностью

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

Итак, у меня есть массив данных, который выглядит примерно так:

const arr = [{
  "content": "FIRST_CONTENT_HERE",
  "callback": function callback () {}
},
{
  "content": "SECOND_CONTENT_HERE",
  "callback": function callback () {}
},
{
  "content": "THIRD_CONTENT_HERE",
  "callback": function callback () {}
},
{
  "content": "THIRD_CONTENT_HERE",
  "callback": function callback () {}
}
];

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

Я искал, но в большинстве найденных примеров генераторов и того, как они взаимодействовали с промисами (или асинхронными функциями), примеры всегда показывали что-то вроде этого yield Promise.resolve(x), а это не то, что мне нужно. Я не хочу, чтобы он разрешался сразу — мне нужно, чтобы он дождался выполнения callback. И обратный вызов будет настроен как обработчик onclick (для закрытия диалога) и будет выполняться только тогда, когда это событие произойдет. Таким образом, следующий диалог не должен создаваться до тех пор, пока не будет закрыт предыдущий. Итак, для каждого цикла массива происходит несколько вещей:

  • Создает диалог
  • Настраивает обработчик onclick для этого диалога. В конце концов, callback() должен быть выполнен где-то там.
  • Я могу обернуть обратный вызов в функцию, которая возвращает обещание (или что-то в этом роде), где обещание не разрешается до тех пор, пока не сработает это событие. Поэтому, когда он срабатывает, он одновременно разрешает обещание и выполняет обратный вызов.
  • Когда это происходит, итератор/генератор переходит к следующей итерации и создает экземпляр следующего диалога. И затем следующий, пока исходный массив не будет зациклен

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

Как мне лучше всего реализовать функциональность, которую я пытаюсь достичь? Как я уже сказал, я просто не могу уложить это в голове...

Вот пример кода, который я написал. Это претерпело несколько изменений, поскольку я пробовал разные вещи. Я использую здесь fetch(), потому что знаю, что она возвращает обещание. Я убежден, что обещания должны быть где-то в моем решении, чтобы убедиться, что мой неасинхронный callback() выполняется до того, как управление перейдет. Это глупо, но я просто пробовал разные доказательства концепции.

      async function runRequest(url) {
        const response = await fetch(url);

        return response;
      }

      async function* setupGenerator(arr) {
        for (let i = 0; i < arr.length; i++) {
          const response = await runRequest(arr[i]);
          yield response;
        }
      }

      const urls = [
        '/activityViewer/index.html',
        '/message/index.html',
        '/users/index.html',
      ];
debugger;
      const generator = setupGenerator(urls);
      for (let response of generator) {
          console.info('In FOR loop; response; ', response);
      }

спасибо,
Кристоф

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

phuzi 11.04.2023 16:32

эти функции возвращают обещания или принимают аргументы обратного вызова? потому что, если нет, JavaScript является однопоточным, поэтому вы не можете «уступить контроль», пока вызываемая функция не вернется или не получит неявный возврат, после чего функция будет выполнена, и вы не сможете выполнять какую-либо другую работу

Samathingamajig 11.04.2023 16:35

@phuzi, извини. Ты прав. Я обновил структуру.

Christoph 11.04.2023 16:41
After I called the generator function and iterated through it покажи этот код
James 11.04.2023 16:42

Вы не можете сказать, была ли выполнена произвольная функция, но вы можете сказать, выполнялась ли она в вашей обернутой версии. Рассмотрите возможность инкапсуляции их в триггерную функцию, например function didRun(fn, hook), которая вызывает hook(), а затем выполняет fn() при вызове, пересылая все аргументы.

tadman 11.04.2023 16:42

@Samathingamajig, нет, они не возвращают обещания, но я могу обернуть их в обещание (). Это то, что я пытался сделать изначально.

Christoph 11.04.2023 16:43

@Christoph, заключающий их в обещание, ничего не меняет, вы можете передать управление только из-за асинхронных вещей, таких как setTimeout, fetch, AsyncLocalStorage, функций, которые возвращают обещания, функций, которые принимают аргументы обратного вызова и т. д.

Samathingamajig 11.04.2023 16:48

Ваш пример нуждается в более подробной информации, показывающей реальные примеры того, что представляют собой функции и что вы на самом деле пробовали. Не говорите просто «мои попытки заглушить простой пример не увенчались успехом», покажите свои попытки. Отредактируйте свой вопрос, не показывайте код в комментариях!

Ruan Mendes 11.04.2023 16:49

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

Bergi 11.04.2023 17:08

@Bergi, как я уже упоминал в своем вопросе, обратный вызов будет использоваться как часть обработчика события onclick. Когда это событие происходит, выполняется обратный вызов. Теперь я думаю, что обработчик событий на самом деле должен быть функцией, которая обертывает обратный вызов. И он устанавливает обещание, которое разрешается только тогда, когда это событие действительно происходит и выполняется callback().

Christoph 11.04.2023 17:22

@Christoph Да, чтобы определить, что событие было запущено и был вызван обратный вызов, вам нужно обернуть обратный вызов перед настройкой прослушивателя событий. Но это все еще не отвечает на вопрос, что на самом деле делает обратный вызов (он действительно нужен? Содержит ли он бизнес-логику?), и где находится код, который регистрирует его (или обернутую функцию) в качестве обработчика кликов.

Bergi 11.04.2023 17:27

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

Bergi 11.04.2023 17:31

@Bergi, да, он содержит бизнес-логику. Он используется для отслеживания того факта, что пользователю было представлено диалоговое окно, и он фактически щелкнул кнопку/ссылку, чтобы закрыть его.

Christoph 11.04.2023 17:31

@ Берги, это честно. Я не уверен на 100%, что мне нужен генератор. Просто кажется, что генераторы - это способ перебирать массив и ждать, пока не произойдет конкретное событие (не событие типа dom; например, Promise.resolve()), прежде чем оно перейдет к следующей итерации. Если я могу каким-то образом убедиться, что обещание не выполняется до определенного момента времени (например, когда нажата кнопка), то я могу сделать так, чтобы цикл мог оставаться в стазисе (ожидая yield или подобного) до тех пор, пока затем.

Christoph 11.04.2023 17:33

Вы должны использовать либо обратные вызовы с бизнес-логикой, либо функцию-генератор (или просто функцию async с некоторыми await) с бизнес-логикой, но не то и другое одновременно.

Bergi 11.04.2023 18:34

Если бы вы могли добавить к своему вопросу код бизнес-логики (при необходимости сокращенный) с псевдокодом, где вы хотите подождать, я могу написать ответ с ним.

Bergi 11.04.2023 18:36

@Bergi, обратный вызов выполнит fetch конечную точку, используемую для отслеживания того факта, что пользователь закрыл диалог. Но я хочу вызывать это fetch только тогда, когда запускается событие клика. То есть, *частично*, где в игру вступает ракурс промиса. Либо .then() вызывает Promise.resolve() из созданного промиса, либо мы просто awaitfetch, я не уверен. Тем не менее, по-прежнему имеет место случай, когда обратный вызов (либо сам по себе, либо функция, которая его обертывает) будет выполняться только для события onclick и только затем возвращает управление обратно в цикл для итерации к следующему элементу массива.

Christoph 11.04.2023 18:52

Итак, опять же, X диалогов будут представлены для X элементов массива, но только после закрытия предыдущего диалога.

Christoph 11.04.2023 18:54
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
В настоящее время производительность загрузки веб-сайта имеет решающее значение не только для удобства пользователей, но и для ранжирования в...
Безумие обратных вызовов в javascript [JS]
Безумие обратных вызовов в javascript [JS]
Здравствуйте! Юный падаван 🚀. Присоединяйся ко мне, чтобы разобраться в одной из самых запутанных концепций, когда вы начинаете изучать мир...
Система управления парковками с использованием HTML, CSS и JavaScript
Система управления парковками с использованием HTML, CSS и JavaScript
Веб-сайт по управлению парковками был создан с использованием HTML, CSS и JavaScript. Это простой сайт, ничего вычурного. Основная цель -...
JavaScript Вопросы с множественным выбором и ответы
JavaScript Вопросы с множественным выбором и ответы
Если вы ищете платформу, которая предоставляет вам бесплатный тест JavaScript MCQ (Multiple Choice Questions With Answers) для оценки ваших знаний,...
0
18
107
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Вы можете использовать асинхронные итераторы:

const arr = [{
  "content": "FIRST_CONTENT_HERE",
  "callback": function callback (_) { return _; }
},
{
  "content": "SECOND_CONTENT_HERE",
  "callback": function callback (_) { return _; }
},
{
  "content": "THIRD_CONTENT_HERE",
  "callback": function callback (_) { return Promise.resolve(_); }
},
{
  "content": "FOUR_CONTENT_HERE",
  "callback": function callback (_) { return _; }
}
];

async function* asyncGenerator(tasks) {
  for (const task of tasks) {
    yield task.callback(task.content);
  }
}

;(async function() {
  for await (const txt of asyncGenerator(arr)) {
    console.info('->', txt);
  }
})();

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

JDB 11.04.2023 16:57

Похоже, это не отвечает на вопрос, поскольку нет ни прослушивателей событий, ни кода, ожидающего чего-либо.

Bergi 11.04.2023 17:29

Генераторы и промисы имеют разные цели

Я хочу создать генератор для итерации по этому массиву, но не возвращать (выход?) управление обратно к следующей итерации, пока эта функция обратного вызова не будет выполнена.

Проблема в том, что генераторы и промисы имеют принципиально разные предположения. Обещание, по сути, «Я буду работать над этим, пока не закончу, и перезвоню вам, когда оно будет закончено», в то время как генератор, по сути, «Дайте мне знать, когда вы будете готовы к следующему».

Если вы используете генератор, вы должны вернуть значение при следующем вызове функции... вы не можете ждать. Итак, вопрос в том, насколько важно для вас дождаться завершения первого вызова, прежде чем начинать второй вызов?

Если вы можете обрабатывать все вызовы одновременно, то ваш генератор может просто возвращать промисы, а ваш вызывающий код может взять на себя ответственность за их выполнение.

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

Решение 1. Сохраняйте контроль, используя аргумент обратного вызова

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

const arr = [{
  "content": "FIRST_CONTENT_HERE",
  "callback": function callback () {}
},
{
  "content": "SECOND_CONTENT_HERE",
  "callback": function callback () {}
},
{
  "content": "THIRD_CONTENT_HERE",
  "callback": function callback () {}
},
{
  "content": "THIRD_CONTENT_HERE",
  "callback": function callback () {}
}
];

async function processList(itemCallback) {
  for (const item of arr) {
    await item.callback();
    itemCallback(item);
  }
}

async function callingCode() {
  await processList(item => {
    // Do something with item...
  });
}

Решение 2. Позвольте вызывающей стороне управлять, возвращая Promises

Если генератор больше ориентирован на удобство и синтаксическую красоту, чем на производительность или технические требования, то вы можете просто вернуть промисы и позволить вызывающему коду обработать их:

const arr = [{
  "content": "FIRST_CONTENT_HERE",
  "callback": async function callback () {}
},
{
  "content": "SECOND_CONTENT_HERE",
  "callback": async function callback () {}
},
{
  "content": "THIRD_CONTENT_HERE",
  "callback": async function callback () {}
},
{
  "content": "THIRD_CONTENT_HERE",
  "callback": async function callback () {}
}
];

async function* getList() {
  for (const item of arr) {
    yield item.callback().then(() => item);
  }
}

async function callingCode() {
  for await (const item of getList()) {
    // Do something with item
  }
}

«Однако в этом случае вызывающий код должен будет дождаться разрешения промиса». Это то, что я хочу, потому что это вызывающий код, который будет настраивать и запускать диалог, к которому будет привязана функция обратного вызова (через обработчик onclick)

Christoph 11.04.2023 17:08

Возможно, но ваш вопрос немного запутан: «Я хочу создать генератор для итерации по этому массиву, но не возвращать (выход?) управление обратно к следующей итерации, пока эта функция обратного вызова не будет выполнена». У генератора нет возможности не сдаваться. Он должен либо уступить, либо вернуться и закончить. Итак, либо вы должны дать обещание, либо вы не можете использовать генератор.

JDB 11.04.2023 17:14

Я обновил свой ответ, чтобы устранить этот нюанс.

JDB 11.04.2023 17:18

«Итак, либо вы должны дать Обещание, либо вы не можете использовать генератор». Вот почему я убежден, что промисы должны быть где-то в моем решении. Я просто не знаю, где и как. Мои кусочки головоломки не складывались воедино.

Christoph 11.04.2023 17:20

Да, промисы имеют большое значение. Таким образом, основной вопрос заключается не в том, «обещает или нет», а в том, «есть ли техническая причина, препятствующая тому, чтобы вызывающий код запускал все вызовы одновременно?» Если ваш код/сервис может обрабатывать все эти одновременные вызовы, просто верните промисы и позвольте вызывающему коду разобраться с этим. Если нет, то не используйте генератор.

JDB 11.04.2023 17:23
Ответ принят как подходящий

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

Вот пример:

promiseDialog — это такая функция обратного вызова: для отображения требуется заголовок, а диалоговое окно позволяет пользователю ввести некоторые данные и закрыть диалоговое окно.

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

Наконец, вы можете использовать эти полученные промисы с помощью цикла for await:

(Примечание: диалог на самом деле не является модальным диалогом, но он не имеет отношения к демонстрации)

function dialogPromise(title) {
    const dialog = document.querySelector("#dialog");
    dialog.querySelector("p").textContent = title;
    dialog.style.display = "inline-block";
    const button = dialog.querySelector("button");
    const input = dialog.querySelector("input");
    input.value = "";
    input.focus();
    return new Promise(function(resolve) { 
        button.addEventListener("click", function (e) {
            e.preventDefault();
            dialog.style.display = "none";
            // Resolve when paint cycle has happened (to hide dialog)
            requestAnimationFrame(() => requestAnimationFrame(() =>
                resolve(input.value)
            ));
        }, { once: true });
    });
}

async function* chainDialogs(tasks) {
    for (const task of tasks) {
        yield task.callback(task.content);
    }
}

(async function() {
    const arr = [
        { "content": "FIRST_CONTENT_HERE",  "callback": dialogPromise }, 
        { "content": "SECOND_CONTENT_HERE", "callback": dialogPromise },
        { "content": "THIRD_CONTENT_HERE",  "callback": dialogPromise },
        { "content": "FOUR_CONTENT_HERE",   "callback": dialogPromise }
    ];
    for await (const result of chainDialogs(arr)) {
        console.info(result);
    }
})();
#dialog { position: absolute; display: none; margin: 5vw ; padding: 5px; border: 1px solid; background: #ee8 }
<form id = "dialog">
    <p>Test dialog</p>
    Your answer: <input>
    <button>OK</button>
</form>

Это именно то, что я ищу. Большое спасибо. Большое спасибо всем, кто прислал ответы и/или прокомментировал. Я узнал много нового из ваших комментариев, и я ценю это. Спасибо.

Christoph 12.04.2023 15:35

Другие вопросы по теме