#javascript #asynchronous #event-handling
#javascript #асинхронный #обработка событий
Вопрос:
Я ищу шаблон о том, как решить типичную проблему, когда несколько (пользовательских) событий запускаются вскоре друг за другом, и каждое событие должно заботиться о некоторой тяжелой нагрузке, которая должна быть обработана до конца, прежде чем новое событие может быть принято.
Типичным вариантом использования был бы, например, список элементов, на которые пользователь может щелкнуть, и для каждого элемента требуется некоторая асинхронная загрузка и обработка. Пользователь нажимает на элемент, который запускает асинхронную обработку. Во время обработки события не следует запускать новую обработку при выборе другого элемента, и в лучшем случае (производительность) потребуется обработать только последнее событие.
Следующий сокращенный код имитирует вариант использования, а вывод на консоль показывает, как событие вызывает проблемы, а затем обрабатывается. Функция run
имитирует (пользовательские) события, а функция heavyLifting
имитирует некоторую асинхронную тяжелую работу. Обрабатывается больше событий, чем необходимо, и, убедившись, что heavyLifting
функция вызывается только последовательно с использованием currentlyLifting
семафора, также больше не гарантируется, что по крайней мере самое последнее событие всегда обрабатывается.
Используйте JSFiddle для запуска сокращенного кода
const getRandom = (min, max) => Math.floor(Math.random() * (max - min) min);
const heavyLifting = id => {
return new Promise(resolve => {
const cost = getRandom(500, 750)
console.log(`heavy lifting "${id}" with cost "${cost}" started...`);
setTimeout(() => {
console.log(`heavy lifting "${id}" completed.`);
resolve();
}, cost);
});
};
let currentlyLifting;
window.addEventListener('heavyLift', async (e) => {
const id = e.detail.id;
if (currentlyLifting !== undefined) {
console.log(`cannot start new heavy lifting of "${id}" while still running "${currentlyLifting}"`);
} else {
currentlyLifting = id;
await heavyLifting(id);
currentlyLifting = undefined;
}
});
const run = () => {
let id = 1;
const timerCallback = () => {
console.log(`dispatchEvent "${id}"`);
window.dispatchEvent(new CustomEvent('heavyLift', {detail: {id}}));
if (id < 10) {
setTimeout(timerCallback, 100);
}
};
timerCallback();
};
run();
<!DOCTYPE html>
<html>
<body>
<script src="index.js"></script>
</body>
</html>
Комментарии:
1. Просто запомните последнее событие и после
await heavyLifting(id);
проверьте, является ли последнее событие тем, которое вы только что обработали, или есть новое, которое нужно обработать.2. @Bergi Это интуитивно возможный шаблон, но я не вижу способа предотвратить поступление дополнительных событий при обработке нового. У вас есть рабочий пример вашего подхода?
3. Вам не нужно предотвращать их поступление, вам просто нужно, чтобы они не начинали обработку.
4. Я согласен, но как бы вы это сделали на основе моего сокращенного кода? Я думаю, вы бы сохранили семафор в обработчике событий, но дополнительно отслеживали бы последнее событие, которое должно быть обработано, и после
await heavyLifting(id);
завершения обработали потенциально последнее пропущенное событие. Как вы можете предотвратить то, что при обработке этого другие не поступают, и мы возвращаемся к исходной точке?5. Да, сохраните семафор. Вы предотвращаете обработку вновь поступающих событий, как вы уже делаете, с
if (currentlyLifting !== undefined)
.
Ответ №1:
Вы могли бы дождаться предыдущего обещания обработки, прежде чем выполнять следующую обработку:
const oneAtATime = () => {
let previous = Promise.resolve();
return action => previous = previous.then(action);
};
const lifting = oneAtATime();
lifting(async function() {
//...
});
lifting(async function() {
//...
});
Или дождаться, пока все текущие действия будут выполнены просто
await lifting(); // don't do that in a lifting task itself, that will cause a livelock
Комментарии:
1. Я понимаю ваш подход сериализации асинхронного снятия большой нагрузки, но, похоже, не могу понять, как это вписывается в мой фрагмент кода, который получает отдельные события? Не были бы вы так добры показать мне, как бы вы это сделали в моем сокращенном коде? Я также не уверен, как это уменьшит количество событий, которые должны быть отменены?
2. @boberkofler возможно, я неправильно понимаю ваш вариант использования: что произойдет, если асинхронное действие уже выполнено и запускается новое? Это должно подождать? Или это следует отменить?
3. В моем случае использования я не могу запустить новый, пока один уже запущен, поэтому я могу только пропустить обработку. Целью было бы пропустить как можно больше, как при отмене, но убедиться, что последнее событие всегда обрабатывается.
4. @doberkofler ну, тогда код в вопросе — это правильный путь, не уверен, смогу ли я помочь вам дальше
5. Мой сокращенный код показывает, что он работает не так, как должен, потому что простым пропуском событий нельзя гарантировать, что всегда обрабатывается последнее событие. Я все еще ищу решение этой проблемы.