Как упорядочить пользовательские события, которые вызывают некоторую интенсивную асинхронную обработку

#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. Мой сокращенный код показывает, что он работает не так, как должен, потому что простым пропуском событий нельзя гарантировать, что всегда обрабатывается последнее событие. Я все еще ищу решение этой проблемы.