Promise all блокирует другой вызов выборки

На самом деле я использую некоторый удаленный API, используя FETCH API, как показано ниже.

document.addEventListener("DOMContentLoaded", () => {
    loadCart();
    aggregateSearch("france", ["api1", "api2"]);
});

const aggregateSearch = async (term, AvailableApis) => {
    console.info("aggregateSearch")
    await Promise.all(AvailableApis.map((api) => {
        fetchApi(api, term);
    }))
};

const fetchApi = async (api, term) => {
    console.info("fetchApi");
    const aggregateRawResponse = await fetch('aggregate.php', { method: 'POST', body: JSON.stringify({ source: api, term: term }) });
    const aggregateResponse = await aggregateRawResponse.json();

    console.info('response from API done');

    document.getElementById('myDiv' + api)?.innerHTML = aggregateResponse.formattedResponse;
}

const updateCartCount = async () => {
    console.info("update cartcount"); // this line is executed
    const fetchNbPhotos = await fetch("count.php");
    const data = await fetchNbPhotos.json();
    console.info("cart count done");
    document.getElementById('cartCounter').innerHTML = parseInt(data["nb"]); // this is executed once aggregateSearch done
}

const loadCart = () => {
    console.info("start loading cart");
    fetch('cart.php')
    .then((response) => {
        return response.text();
    })
    .then((html) => {
        console.info("loading cart is done updating cart count");
        document.getElementById('cart')?.innerHTML = html
        updateCartCount();
    });
}

Здесь вывод консоли

start loading cart
aggregateSearch
fetchApi
loading cart is done updating cart count
update cartcount
cart count done
response from API done

Часть, которая извлекает содержимое API, работает отлично, но часть, которая загружает мою корзину в cart div и обновляет мой cartCounter div, обновляется только после завершения aggregateSearch.

Я пытался сделать loadCart асинхронным/ожидающим, но безуспешно.

Как я могу сделать aggregateSearch неблокирующим и обновить свой cartCounter div, пока ожидается получение API?

РЕДАКТИРОВАТЬ

Я попытался изменить aggregateSearch, как показано ниже.

const aggregateSearch = async (term, AvailableApis) => {
    console.info("aggregateSearch")
    await Promise.all(AvailableApis.map((api) => {
        setTimeout(() => fetchApi(api, fulltext, []), 100);
        //fetchApi(api, fulltext, []);
    }))
};

И мой идентификатор cartCounter div мгновенно обновился.

РЕДАКТИРОВАТЬ2

Вот воспроизводимый пример https://jsfiddle.net/3sdLweag/26/

Консольный вывод

"start loading cart"
"aggregateSearch"
"update cartcount"
"response from API done"
"cart count done"

Поскольку fetchApi имеет задержку 2000, loadCart и updateCartCount имеют задержку 100, оба ожидаемых результата должны быть

"start loading cart"
"aggregateSearch"
"update cartcount"
"cart count done"
"response from API done"
await Promise.all(AvailableApis.map((api) => { fetchApi(api, term); })) не ждёт обещаний, так как никаких обещаний от этого звонка не даёт. Просто массив undefined, который будет немедленно решен. Либо добавьте return, либо удалите фигурные скобки.
VLAZ 09.08.2024 16:08

массив undefined ? fetchApi является асинхронным, console.info(fetchApi(api, term)) возвращает Promise {<pending>}

akio 09.08.2024 16:19

Стрелочной функции с фигурными скобками требуется явный оператор return, чтобы что-то вернуть. (api) => { fetchApi(api, term); } возвращает undefined, а не ожидаемое обещание. Либо удалите фигурные скобки (api) => fetchApi(api, term), либо добавьте оператор return(api) => { return fetchApi(api, term); }

jabaa 09.08.2024 16:22

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

VLAZ 09.08.2024 16:33

На самом деле это не совсем блокировка, но пока aggregateSearch не будет выполнено и document.getElementById('myDiv' + api)?.innerHTML не выполнено, мой контент cartCounter div не обновится. Если я оберну вызов fetchApi с помощью setTimeout 100 мс, он обновится немедленно.

akio 09.08.2024 16:53

Можете ли вы предоставить минимальный воспроизводимый пример с имитируемой fetch функцией?

jabaa 09.08.2024 17:03

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

akio 09.08.2024 21:27

С вашим кодом есть несколько проблем, но я не понимаю, почему вы говорите: «Обновление моего div-карты CarCounter обновляется только после завершения агрегатного поиска», потому что из предоставленных вами результатов мы видим, что HTML обновляется в следующем порядке: cart, потом cartCounter и потом myDiv+api. Кажется, это тот порядок, который вам нужен? Каков порядок вывода, который вы ожидали?

trincot 10.08.2024 07:27

В моем приложении cart count done и response from API done выполняются одновременно, поэтому все элементы html заполняются одновременно, а не после выполнения выборки.

akio 10.08.2024 07:37

Все ваши запросы независимы друг от друга, поэтому не существует гарантированного порядка их выполнения и обновлений DOM. (И похоже, вы его не хотите?). Однако все они по-прежнему отправляются к одному и тому же серверу/домену, и ваш браузер может регулировать это, чтобы разрешить лишь небольшое количество одновременных подключений на домен. По этой причине запросу (cartcount), который начался последним, возможно, придется подождать, пока он действительно будет отправлен, пока не завершатся предыдущие запросы.

Bergi 10.08.2024 15:55
Поведение ключевого слова "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
11
90
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Надеюсь, я понял вашу проблему.

Я обнаружил, что асинхронные функции могут по-прежнему мешать друг другу при работе с одним и тем же документом, особенно при изменении элементов. Хотя они выполняются одновременно, выполнение каждой строки кода все равно занимает определенное время (особенно console.info()). Они могут работать одновременно, но они также более или менее работают в одном и том же основном потоке.

Я столкнулся с этой проблемой при попытке реализовать символы загрузки или индикаторы выполнения. Мои символы загрузки заикались, а индикаторы выполнения менялись от 0% до 100% без каких-либо других действий.

Решение, которое я нашел, было Web Workers. Нажмите здесь, чтобы получить подробное руководство по API Web Worker.

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

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

// The function to send to a Worker.
// NOTE that the code in this function won't be able to access variables outside of it or the document directly. That will have to be handled in the message event of our Worker.
function code(amount) {
 var values = [];
 for(var i = 0; i < amount; i++) {
  values.push(Math.random());

  // Send our progress back to the main thread so we can show the user.
  postMessage([(i+1)/amount]);
 }

 // Send our final values.
 postMessage(values);
}

// Create a javaScript file as an Object URL.
var url = URL.createObjectURL(
 new Blob(
  // Place the code() function in the new JavaScript file.
  // Define the function for the Worker's onmessage event to start our function (doing this lets us send actual values and objects to the Worker and start it on demand).
  [`
   var code = ${code};
   onmessage = message => { postMessage(code(...message.data)); };
  `],
  {type: 'application/javascript'}
 )
);

// Create a worker with the script from our JavaScript file.
var customworker = new Worker(url);

// Add our Object URL to the customworker to access it later.
customworker.url = url;

// Add a function to revoke the Object URL when we terminate the worker.
customworker.resolve = function() {
 URL.revokeObjectURL(this.url);
 this.terminate();
};

var progress = document.querySelector('progress');

// Retrieve and process data whenever a message is posted using postMessage().
customworker.addEventListener('message', function(message) {
 if (message.data.length == 1) {
  // Show the user the progress of our function using the progress of the for loop in code().
  progress.value = message.data[0];
 } else {
  // Terminate the worker using our custom function since we know the worker is done.
  // If we want to use the code in the worker again and again, you don't need to do this.
  this.resolve();

  // Show the user what we want to show them of the result.
  document.querySelector('div').innerHTML = `1000000 random numbers were found. The final number was ${message.data.slice(-1)}.`;
 }
});

// Run the function in our Worker sending a parameter of 1000000 to code().
customworker.postMessage([1000000]);
<div>Running Worker...</div>
<progress max='1' value='0'/>

Вот упрощенная версия приведенного выше кода

function code() {
 // Your code here.
 // Use postMessage() to send data back to the document.
}
var url = URL.createObjectURL(new Blob([`var code = ${code}; onmessage = message => { postMessage(code(...message.data)); };`], {type: 'application/javascript'}));
var customworker = new Worker(url);
customworker.url = url;
customworker.resolve = function() { URL.revokeObjectURL(this.url); this.terminate(); };
customworker.addEventListener('message', function(message) {
 // Use message.data and manipulate the document here.
});
customworker.postMessage([/* Parameters go here. */]);

Вы можете использовать Worker в aggragateSearch() или fetchAPI(), чтобы переложить интенсивные требования функции fetch() в новый поток, а затем обновить div, который вы надеетесь изменить, в eventListener 'message' Worker.

Например, ваша функция fetchApi может выглядеть примерно так (непроверенная):

function fetchApi(api, term) {
 fetchApi.worker.postMessage([api, term]);
}
fetchApi.worker = new Worker(
 URL.createObjectURL(
  new Blob(
   [`var code = ${
     (api, term) => fetch('aggregate.php', { method: 'POST', body: JSON.stringify({ source: api, term: term }) })
      .then(result => result.json())
      .then(result => postMessage(result.formattedResponse))
    }; onmessage = message => { postMessage(code(...message.data)); };`],
   {type: 'application/javascript'}
  )
 )
);
fetchApi.worker.addEventListener('message', function(message) {
 document.getElementById('myDiv' + api).innerHTML = message.data;
});

Если вы можете поместить свой код в настоящий файл .js, это может быть чище, чем создание объекта URL при использовании Workers.

Это решение может не удовлетворить ваши потребности, если я неправильно понял вашу проблему — и вам придется адаптировать его к вашему полному коду — но, надеюсь, это будет вам полезно!

Интересный способ, попробую, спасибо

akio 10.08.2024 19:07
Ответ принят как подходящий

Похоже, вы хотите loadCart завершить асинхронные части — заканчивая обновлением cartCounter — до того, как будет начата выборка aggregateSearch, поскольку именно этого, скорее всего, достигнет ваша версия setTimeout.

Тогда решение состоит в том, чтобы дождаться выполнения асинхронных задач, выполненных loadCart.

Прежде всего loadCart должен вернуть обещание. Требуется return в двух местах вашего кода.

const loadCart = () => {
    console.info("start loading cart");
    return fetch('cart.php')  // <-- return the promise!
    .then((response) => {
        return response.text();
    })
    .then((html) => {
        console.info("loading cart is done updating cart count");
        document.getElementById('cart')?.innerHTML = html;
        return updateCartCount(); // <-- return the promise!
    });
}

Или, альтернативно, напишите эту функцию как функцию async, как вы это делали в других местах вашего кода:

const loadCart = async () => { // <-- make it async
    console.info("start loading cart");
    const response = await fetch('cart.php');  // <-- and use await
    const html = await response.text();        // <-- 
    console.info("loading cart is done updating cart count");
    document.getElementById('cart')?.innerHTML = html;
    return updateCartCount(); // <-- return the promise!
}

Другой момент — вы не получите полезного результата от Promise.all, так как не передадите ему массив обещаний. Это делает оператор await для этого Promise.all совершенно бесполезным. В вашем случае это не имеет негативного эффекта, поскольку вы никогда не используете этот результат, но было бы разумнее сделать это правильно и aggregateSearch вернуть обещание, которое разрешается только после завершения асинхронных задач:

const aggregateSearch = async (term, AvailableApis) => {
    console.info("aggregateSearch");
    await Promise.all(AvailableApis.map((api) => {
        return fetchApi(api, term);  // <-- return the promise!
    }));
};

Наконец, дождитесь возвращенных обещаний в обработчике событий:

document.addEventListener("DOMContentLoaded", async () => {  // <-- make it async
    await loadCart(); // <-- await it
    await aggregateSearch("france", ["api1", "api2"]); // <-- await it
});

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

Спасибо за ваш очень подробный отчет 🙏 Я не особо жду завершения loadCart, но знаю, что эта функция работает быстрее, чем aggregateSearch, поэтому вполне нормально ожидать, что сначала нужно обновить cartCounter div

akio 10.08.2024 10:30

«Я знаю, что эта функция быстрее»: ты уверен? Он должен сделать 2 запроса, а в вашем примере поиск делает только 1.

trincot 10.08.2024 11:23

Я проверил jsfiddle во втором сообщении?

akio 10.08.2024 11:47

Когда я запускаю эту скрипту, вывод «подсчет корзин выполнен» идет последним.

trincot 10.08.2024 13:58

Для меня это означает, что время между запуском обработчика событий и выводом «подсчет корзины выполнено» больше, чем время, необходимое для создания вывода «ответ от API выполнен», а это означает, что асинхронная задержка для loadCart занимает больше времени, чем асинхронная задержка для aggregateSearch. Как вы пришли к выводу, что loadCard быстрее, чем aggregateeSearch? Я вижу обратное...

trincot 10.08.2024 14:02

в этом смысле оно должно быть раньше "response from API done", поскольку fetchapi занимает 2 секунды, а loadCart(100 мс) + updateCartCount(100 мс) занимает 200 мс

akio 10.08.2024 14:03

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

trincot 10.08.2024 14:05

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

trincot 10.08.2024 14:09

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

akio 10.08.2024 19:06

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