На самом деле я использую некоторый удаленный 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"
массив undefined
? fetchApi является асинхронным, console.info(fetchApi(api, term))
возвращает Promise {<pending>}
Стрелочной функции с фигурными скобками требуется явный оператор return
, чтобы что-то вернуть. (api) => { fetchApi(api, term); }
возвращает undefined
, а не ожидаемое обещание. Либо удалите фигурные скобки (api) => fetchApi(api, term)
, либо добавьте оператор return
(api) => { return fetchApi(api, term); }
@akio (api) => { fetchApi(api, term); }
— это функция, которая возвращает только undefined
Почему моя стрелочная функция с телом в скобках возвращает неопределенное значение? | Когда мне следует использовать оператор return в стрелочных функциях ES6
Итак, подведем итог: я понятия не имею, в чем проблема, потому что то, что вы описываете, невозможно. aggregateSearch
ничего не должно «блокировать». Он должен разрешить следующий тик - задолго до завершения какой-либо из асинхронных функций, которые он запускает (но не ждет).
На самом деле это не совсем блокировка, но пока aggregateSearch
не будет выполнено и document.getElementById('myDiv' + api)?.innerHTML
не выполнено, мой контент cartCounter
div не обновится. Если я оберну вызов fetchApi
с помощью setTimeout 100 мс, он обновится немедленно.
Можете ли вы предоставить минимальный воспроизводимый пример с имитируемой fetch
функцией?
Странно то, что вместо этого вместо вызова выборки на setTimeout это работает ... есть другой способ имитировать выборку без зависимостей?
С вашим кодом есть несколько проблем, но я не понимаю, почему вы говорите: «Обновление моего div-карты CarCounter обновляется только после завершения агрегатного поиска», потому что из предоставленных вами результатов мы видим, что HTML обновляется в следующем порядке: cart
, потом cartCounter
и потом myDiv+api
. Кажется, это тот порядок, который вам нужен? Каков порядок вывода, который вы ожидали?
В моем приложении cart count done
и response from API done
выполняются одновременно, поэтому все элементы html заполняются одновременно, а не после выполнения выборки.
Все ваши запросы независимы друг от друга, поэтому не существует гарантированного порядка их выполнения и обновлений DOM. (И похоже, вы его не хотите?). Однако все они по-прежнему отправляются к одному и тому же серверу/домену, и ваш браузер может регулировать это, чтобы разрешить лишь небольшое количество одновременных подключений на домен. По этой причине запросу (cartcount), который начался последним, возможно, придется подождать, пока он действительно будет отправлен, пока не завершатся предыдущие запросы.
Надеюсь, я понял вашу проблему.
Я обнаружил, что асинхронные функции могут по-прежнему мешать друг другу при работе с одним и тем же документом, особенно при изменении элементов. Хотя они выполняются одновременно, выполнение каждой строки кода все равно занимает определенное время (особенно 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.
Это решение может не удовлетворить ваши потребности, если я неправильно понял вашу проблему — и вам придется адаптировать его к вашему полному коду — но, надеюсь, это будет вам полезно!
Интересный способ, попробую, спасибо
Похоже, вы хотите 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
«Я знаю, что эта функция быстрее»: ты уверен? Он должен сделать 2 запроса, а в вашем примере поиск делает только 1.
Я проверил jsfiddle во втором сообщении?
Когда я запускаю эту скрипту, вывод «подсчет корзин выполнен» идет последним.
Для меня это означает, что время между запуском обработчика событий и выводом «подсчет корзины выполнено» больше, чем время, необходимое для создания вывода «ответ от API выполнен», а это означает, что асинхронная задержка для loadCart
занимает больше времени, чем асинхронная задержка для aggregateSearch
. Как вы пришли к выводу, что loadCard
быстрее, чем aggregateeSearch
? Я вижу обратное...
в этом смысле оно должно быть раньше "response from API done"
, поскольку fetchapi
занимает 2 секунды, а loadCart
(100 мс) + updateCartCount
(100 мс) занимает 200 мс
Я не вижу, чтобы fetchapi
тратилось 2 секунды, когда я управляю твоей скрипкой. Все заканчивается с гораздо более короткой задержкой. Честно говоря, я не понимаю, чего вы хотите, за исключением того, что я вижу некоторые проблемы в вашем коде, которые я рассмотрел в своем ответе.
Возможно, ответ кэшируется, поэтому я не вижу задержки. Но в любом случае, предлагаемое мной изменение кода не делает того, что вам нужно?
Предложения дают ожидаемый результат, но проблема, с которой я сталкиваюсь, не должна возникнуть. Пока жду причину, добавлю ожидание в функцию loadCart
. Большое спасибо за вашу помощь
await Promise.all(AvailableApis.map((api) => { fetchApi(api, term); }))
не ждёт обещаний, так как никаких обещаний от этого звонка не даёт. Просто массивundefined
, который будет немедленно решен. Либо добавьтеreturn
, либо удалите фигурные скобки.