Выполнить только одно из одновременных событий
Описание
У меня есть некая функция (для ясности назовем foo
), которая должна выполнятся если пользователь опустил курсор на элемент. Поскольку мне неизвестно какое устройство у пользователя я прикрепляю все возможные обработчики:
element.addEventListener(`mousedown`, (event) => {
foo();
});
element.addEventListener(`pointerdown`, (event) => {
foo();
});
element.addEventListener(`touchstart`, (event) => {
foo();
});
Теперь мне надо делать так, чтобы когда в тот же момент заработает один из обработчиков, остальные молчали, чтобы функция выполнилась 1 раз, а не 3. Делаю я это вот так:
const controller = new AbortController();
await new Promise((resolve) => {
element.addEventListener(`mousedown`, (event) => {
resolve();
}, { signal: controller.signal });
element.addEventListener(`pointerdown`, (event) => {
resolve();
}, { signal: controller.signal });
element.addEventListener(`touchstart`, (event) => {
resolve();
}, { signal: controller.signal });
});
foo();
controller.abort();
Работает неплохо, но работает 1 раз. Чтобы заработало заново мне надо будет создать функцию которая сгенерирует эту же конструкцию каждый раз после окончания.
Проблема
Ну решение, как решение работает. Но если считать что кроме mousedown
, pointerdown
, touchstart
ещё и добавить mousemove
, pointermove
, touchmove
и ещё куча таких же обработчиков, то, по-моему, выглядит нудно.
Вопрос
Есть альтернативное, более хорошее решение?
Или же можно упростить эту конструкцию?
ЧаВо
— Почему не использовать только
pointer...
слушатели? Они же универсальные. — Они иногда неправильно работают. К примеру,pointermove
останавливается 24-30 кадров после опускания мыши при использовании на мобильных устройствах.
— Почему не использовать для каждого устройства свои правильные обработчики? — Я бы с радостью. Но как на 100% верно узнать эту "правильность" обработчиков? Обработчики везде присутствуют, но не реагируют.
navigator.userAgent
дает неправильной информации. Какие ещё способы мне попробовать?
Ответы (3 шт):
NB Это была хорошая попытка, но, скорее всего, не заработает как надо. Опора на какие-то интервалы времени, надеясь что всё работает достаточно быстро - это все не надёжно. Я сперва думал что двух миллисекуд хватит. А оказалось что на мощном железе, на пустой странице интервал может быть и больше. Увеличил до десяти миллисекунд. Смотрите сами. Я бы сделал идемпотентный обработчик, хоть это и много работы.
Попробуйте троттлинг в десять миллисекунд:
const throttle = (f, delay) => {
let waitFor = 0;
return (...args) => {
const now = new Date().getTime();
if (now > waitFor) {
waitFor = now + delay;
// setTimeout(() => f(...args));
f(...args);
}
};
};
const addEventListeners = (element, types, listener) => {
const handler = throttle(listener, 10);
types.forEach(type => element.addEventListener(type, handler));
};
...
addEventListeners(
element,
['mousedown', 'pointerdown', 'touchstart'],
event => foo()
);
P.S. Интервал выбран в десять миллисекунд потому что:
Обработка может производится до 100 раз в секунду, пользователь не заметит пропуска разных событий так как обычный темп обработки не более 60 раз в секунду.
Разные логические события произошедшие от одного физического события будут вызываться с малым интервалом по времени.
P.P.S. Хочется написать setTimeout(() => f(...args));
. Но тогда перестанет работать event.preventDefault();
в теле f
, а это часто нужно. С другой стороны f
должен исполняться как можно скорее, иначе троттлинг просто не сработает. Идеально если f
выполнит event.preventDefault();
а всю остальную обработку передаст в вызов setTimeout(...);
.
P.P.P.S. С другой стороны, если вам нужен preventDefault
, то его надо делать без троттлинга во всех обработчиках. Тогда схема другая:
const addEventListeners = (element, types, listener, preventDefault = false) => {
const handler = throttle(listener, 10);
let h = handler;
if (preventDefault) {
h = event => {
event.preventDefault();
handler(event);
};
}
types.forEach(type => element.addEventListener(type, h));
};
...
addEventListeners(
element,
['mousedown', 'pointerdown', 'touchstart'],
event => foo(),
preventDefault = true
);
Короче, не было печали, купила баба порося...
Наверху есть решение с замыканием и созданием всего списка событий из массива. По итогу, лично у меня постоянно срабатывает pointerdown
(к которому у автора есть претензии), независимо от порядка. Ну и фактически создаются все 2-3 варианта события, если они имеются в браузере.
Мое решение - проверять событие из списка на наличие в объекте window
и вешать обработчик первое не равное undefined
. Из плюсов - можно задавать любой порядок, не создает лишних событий. Из минусов завязано на проверку наличия событий в объекте window
.
ВАЖНО!! ключевой момент порядок обработчиков в массиве, на мой взгляд, оптимальный touch, mouse, pointer - но этот момент уточняется на практике
const addEventListeners = (element, types, listener) => {
for(let i = 0; i < types.length; i++) {
if (window[`on${types[i]}`] !== undefined) {
element.addEventListener(types[i], e => listener(e));
return;
}
}
};
const foo = e => {
console.log(e.type, 'event');
result.insertAdjacentHTML('beforeend', `<div>${e.type}, event</div>`);
}
const element = document.querySelector('.button');
const result = document.querySelector('.result');
addEventListeners(
element,
['touchstart','mousedown', 'pointerdown'],
event => foo(event)
);
<button class='button'>кнопка</button>
<div class='result'></div>
Идея отсюда Существование события в браузере , и еще можно здесь посмотреть Как ваш браузер обрабатывает прикосновения к экрану телефона (js touch events).
Первое полученное событие замыкает обработчик на себя. Он перестаёт реагировать на события других типов:
const makeGate = f => {
let password = undefined;
return (key, event) => {
if (password === key) {
f(event);
} else if (password === undefined) {
password = key;
f(event);
}
};
};
const addEventListeners = (element, types, listener) => {
const gate = makeGate(listener);
types.forEach(
type => element.addEventListener(type, event => gate(type, event))
);
};
...
addEventListeners(
element,
['mousedown', 'pointerdown', 'touchstart'],
event => foo()
);