События OnMouseDown и onClick на JavaScript в сравнении с очередью событий

#javascript #mouseevent #eventqueue

Вопрос:

У меня есть следующий простой JS-код (https://stackblitz.com/edit/web-platform-ueq5aq?file=script.js ):

 const baton = document.querySelector('button');

baton.addEventListener('mousedown', (e) => {
  console.log('baton');
  baton.addEventListener('click', (e) => {
    console.log('baton click');
  });
});
 

Когда я нажимаю на кнопку, я получаю «батон» и «щелчок батона», зарегистрированные в консоли. Теперь мой вопрос в том, что именно здесь происходит? Насколько я понимаю, в момент выполнения скрипта обработчик mousedown добавляется в четную очередь. Когда я на самом деле нажимаю кнопку, этот обработчик запускается, поэтому он берется из очереди событий, добавляется в стек вызовов и выполняется. Когда он выполняется, обработчик «click» добавляется в очередь событий.

Как на самом деле событие onClick запускается после onMouseDown ? Как это связано с очередью событий? Почему onMouseDown обработчик запускается до click того, как произойдет событие? Я спрашиваю, потому что у меня есть гораздо более сложный код, где результат отличается в разных сценариях.

Когда пользователь переходит на страницу в SPA, которая содержит аналогичный скрипт, а затем нажимает кнопку «baton», заказ:

событие наведения курсора мыши -> обработчик нажатие курсора мыши -> обработчик щелчок -> событие щелчка

И когда пользователь перезагружает страницу, so SPA загружается прямо на этой странице, и нажимает кнопку «baton», чтобы:

событие наведения курсора мыши -> событие щелчка мыши -> обработчик наведения курсора мыши

Я ищу ответ и истину. Любая помощь будет очень признательна.

Ps. К сожалению, я не могу воспроизвести эту ошибку в репозитории примеров — это происходит в довольно сложном веб-приложении, производственным кодом которого я не могу поделиться здесь по понятным причинам.

Ps2. Просто чтобы уточнить, потому что, вероятно, это недостаточно четко указано: я спрашиваю не «почему событие mousedown запускается перед событием click», а «почему ОБРАБОТЧИК mousedown запускается перед событием click». Это НЕ очевидно, потому что обработчики запускаются не сразу. Чтобы запустить обработчик, он сначала должен дождаться, пока стек вызовов опустеет, чтобы очередь событий могла быть обработана JS engine.

Комментарии:

1. Просто предупреждаю, но ваша ссылка на stackblitz не работает (может быть, она частная?)

Ответ №1:

Браузер отслеживает элемент, на котором вы нажали мышкой вниз. Затем он отслеживает элемент, на котором вы нажали кнопку мыши. Если элемент, на котором вы подняли кнопку мыши, является тем же элементом или дочерним элементом целевого элемента. Затем событие щелчка отправляется на последний элемент, на который вы навели курсор мыши. Затем событие распространяется вверх по цепочке элементов на каждый родительский элемент, если только событию не будет указано прекратить распространение.

Если вы нажмете на элемент A и наведете курсор мыши на элемент B. Тогда A получит событие мыши вниз, а B получит событие мыши вверх, но ни одно из них не получит событие щелчка. То же самое, если вы переходите в браузере на другую страницу между нажатием мыши вниз и мыши вверх.

Комментарии:

1. В моем примере я всегда нажимаю на baton элемент, и как наведение курсора мыши, так и наведение курсора мыши (и щелчок) происходят в одном и том же элементе и всегда на одной странице — либо это страница B SPA (загружается напрямую), либо страница B загружается путем перехода со страницы A. Тем не менее, в этих двух сценариях результаты разные.

2. @Furman ты плохо объясняешь свою ситуацию. Вы либо что-то пропускаете, либо не используете правильные слова. Было бы полезно, если бы вы могли предоставить рабочий образец вашей проблемы. Ссылка в вашем сообщении недоступна.

3. Извините, я старался быть как можно более понятным, но, может быть, еще раз: у меня ситуация, когда, если пользователь загружает страницу напрямую example.com/test и он нажимает кнопку «baton» в порядке «событие наведения курсора мыши -> событие нажатия мыши -> обработчик наведения курсора мыши». Когда пользователь переходит на страницу example.com/test от example.com/other (и обратите внимание, что это SPA, поэтому страница не перезагружается) порядок другой: «событие наведения курсора мыши -> событие щелчка мыши -> обработчик наведения курсора мыши». Навигация между наведением курсора мыши и щелчком мыши отсутствует.

4. В случае перехода от «/ example» к «/ test» к документу и другим элементам прикреплено гораздо больше обработчиков, вот почему я подозреваю, и я спрашиваю об очереди событий. Я подозреваю, что здесь происходит какое-то состояние гонки, но мне нужно понять детали того, что здесь происходит, чтобы подтвердить это.

Ответ №2:

Из MDN Web Docs

Элемент получает событие щелчка, когда кнопка указывающего устройства (например, основная кнопка мыши) нажата и отпущена, пока указатель находится внутри элемента.

Итак, есть mouseup событие, а затем click событие.


РЕДАКТИРОВАТЬ после вопроса редактировать:

«почему ОБРАБОТЧИК НАВЕДЕНИЯ КУРСОРА мыши запускается перед событием щелчка?»

Ваш уже выполняющийся mousedown обработчик регистрирует click обработчик, так как же click обработчик должен запускаться перед ним?

Все click обработчики, зарегистрированные во всех предыдущих mousedown обработчиках, также будут запускаться после mousedown mouseup событий и.

введите описание изображения здесь

Комментарии:

1. Да, это совершенно очевидно, но это не то, о чем я просил, это не объясняет пример, который я привел в вопросе

2. после события mouseup уже зарегистрирован обработчик кликов. но я думаю, даже если вы зарегистрируете щелчок мышью, он будет работать

3. Но обработчик mousedown должен проходить через очередь событий, это происходит не сразу. Мой вопрос в том, почему на самом деле здесь это происходит до click события? Почему в некоторых ситуациях (как я описал) это происходит после click события? Я предполагаю, что здесь может иметь значение размер очереди событий, но я прошу подробного объяснения, чтобы понять, что на самом деле происходит здесь «за кулисами».

4. щелчок никогда не происходит до нажатия мыши (как я цитирую в своем ответе). mousedown и mouseup происходят из аппаратного обеспечения, а click — это «виртуальное» событие мыши, основанное на паре down и up

5. @Furman моя точка зрения в комментарии прямо выше заключается в том, что обработчик всегда будет запускаться первым, потому что события наведения курсора мыши должны происходить до событий щелчка.. времени, которое требуется человеку для перехода от наведения курсора мыши к щелчку, более чем достаточно, чтобы очередь событий была пустой, что означает, что наведение курсора мыши (и его обработчики) всегда будет происходить до щелчка (и его обработчиков) (если не задействован какой-либо бот).. если это не ответ на ваш вопрос.. вы определенно не задали правильный вопрос ;-;

Ответ №3:

Возможно, нам следует начать с прояснения нескольких вещей.

События в браузере моделируются скорее как «иерархия вложенности», а затем очередь — то, как это работает, называется пузырением событий — [Википедия][1]

Но, по сути, то, что вы делаете при добавлении EventListener, подключается к одной или нескольким точкам DOM и говорит: «Эй, когда здесь проходит событие X, используйте функцию Y для его обработки, прежде чем передавать его вверх по стеку.

После того, как EventListener был «добавлен», он остается активным в ожидании получения события. Что именно он делает, определено в его функции-обработчике.

 let myYFunction = function( e ) { ... }

let myXListener = baton.addEventListern('X event', myYFunction );

// at this point, anytime 'X event' happens to baton, myYFunction will 
// be called to handle it...
 

Теперь давайте посмотрим на ваши примеры, давайте немного разберем вещи,

 const baton = document.querySelector('button');
 

Эта первая строка просто запрашивает DOM, чтобы найти первый элемент типа ‘button’ на странице. Правильно… Это «куда» мы хотим вставить наш обработчик событий. Мы могли бы добавить их к любому элементу, в любом месте DOM, мы могли бы даже подключиться к элементу ‘body’, если бы захотели.

Хорошо, тогда у вас есть этот бит,

 baton.addEventListener('mousedown', (e) => {
  console.log('baton');
  baton.addEventListener('click', (e) => {
    console.log('baton click');
  });
});
 

Который «вкладывает» создание прослушивателя событий «click», но только после того, как событие «mousedown» было «обработано». Нет никакой реальной причины, по которой событие ‘click’ должно было быть зарегистрировано в теле функции обработчика mousedown.

Если мы немного перепишем это, может быть понятнее, что происходит на самом деле.

 baton.addEventListener('mousedown', (e) => {
  console.log('baton mousedown');
}

baton.addEventListener('click', (e) => {
    console.log('baton click');
});
 

Кроме того, я бы также отметил, что то, как это делается в настоящее время, «работает», но на самом деле это скрывает немного неаккуратного кодирования… вы видите, что каждый раз, когда запускается событие «mousedown», регистрируется новый список событий «click»… таким образом, в конечном итоге вы можете столкнуться со многими, многими, многими обработчиками щелчков, реагирующими на одно событие «щелчка»… Ознакомьтесь с MDN, чтобы узнать больше об [этом] [2]


Я надеюсь, что это ответит на ваши первоначальные вопросы относительно того, что происходит.


На ваш вопрос: «Когда я нажимаю кнопку, я получаю «baton» и «baton click», зарегистрированные в консоли. Теперь мой вопрос в том, что именно здесь происходит? » — Для меня это выглядело бы примерно так:

  1. добавлен список событий ‘mousedown’, однако ничего не «выполняется».
  2. происходит событие «mousedown», теперь ваш прослушиватель «mousedown» выполняет свою функцию, которая, в свою очередь, выходит из системы на консоль и регистрирует новый обработчик «click», но, опять же, не выполняется.

Двигаясь вперед, шаги 1 и 2 повторяются для каждого «наведения курсора мыши», видимого baton. Кроме того, для любого события «щелчка», переданного через baton, которое происходит после каждого наведения курсора мыши на baton:

  1. Происходит событие «щелчок», затем выполняется ваш обработчик «щелчка» и он выходит из системы на консоль.

Стратегии обработки событий SPA

При работе с SPA, где отображаются несколько «страниц», при одной загрузке страницы … это может привести к беспорядку, все эти прослушиватели событий, зависающие вокруг, нагромождаются друг на друга. Если вы собираетесь использовать EventListeners между «страницами» вашего SPA, вы можете захотеть изучить, как их «удалить». — [MDN][3]

Таким образом, у вас будут активны только EventListeners для текущей «Страницы» вашего SPA.

Кроме того, рассмотрите возможность «обобщения» ваших обработчиков и прикрепления их выше в DOM… Это позволило бы вам иметь только несколько прослушивателей событий, которые «направляют» события к их «логическим» обработчикам.


Случайное / разное поведение

С описанными выше шагами, 1, 2 и 3 и тем, как они не все происходят одновременно. Вы увидите то, что кажется случайным выводом на консоль … попробуйте запустить что-то подобное, чтобы получить правильное представление о вещах:

 let cCount = 0;
let mCount = 0;
let tCount = 0;

const baton = document.querySelector('button');

baton.addEventListener('mousedown', (e) => {
  console.log('mousedown # '   (mCount  )   ' order:'   tCount  );
  baton.addEventListener('click', (e) => {
    console.log('click # '   (cCount  )   ' order:'   tCount  );
  });
});
 

[1]: https://en.wikipedia.org/wiki/Event_bubbling#:~:text=Event bubbling is a type,Provided the handler is initialized).
[2]: https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener
[3]: https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/removeEventListener

Комментарии:

1. Это качественный и подробный ответ, спасибо за ваши усилия, но я не могу согласиться с некоторыми моментами. О части первой: baton.addEventListener(‘mousedown’, (e) => { console.log(‘baton’); baton.addEventListener(‘click’, (e) => { console.log(‘щелчок дубинкой’); }); }); определенно неравносильно присоединению двух обработчиков отдельно один за другим (как вы написали «Если мы немного перепишем это, может быть понятнее, что происходит на самом деле». В моем примере я написал, что обработчик щелчков привязывается условно после запуска обработчика mousedown. Если в обработчике mousedown мы отключаем прослушиватель событий щелчка

2. это никогда не произойдет. И это имеет место в моем «производственном» коде. О части «происходит событие «mousedown», теперь ваш прослушиватель «mousedown» выполняет свою функцию». Я также не могу согласиться здесь. Когда происходит событие mousedown, прослушиватель mousedown не выполняется немедленно. Он будет выполнен, как только стек вызовов опустеет, и предыдущие дескрипторы из очереди событий будут выполнены.

3. О вашей части «обработка SPA» — да, я знаю об удалении неиспользуемых прослушивателей событий и утечках памяти, я все это знаю. Но в реальном сценарии это не так просто. Это сложное приложение, когда некоторые прослушиватели не могут быть отсоединены, поскольку они поступают из внешних библиотек (например, sentry).

4. В целом спасибо за хороший пост, но, боюсь, он не объясняет мой вопрос. Мне нужно понять, «почему ОБРАБОТЧИК mousedown запускается до / после (в зависимости от ситуации) события щелчка». То, как срабатывает событие щелчка, связано с обработчиками, ожидающими запуска в очереди событий. Я нигде не смог найти объяснения, и ответы на этот вопрос (несмотря на то, что я не сомневаюсь, что авторы приложили все усилия, чтобы предоставить наилучшее и подробное объяснение) также не объясняют этого.

5. Что касается того, почему когда-то до, а иногда и после, это неблокирующие асинхронные функции, если вам нужно гарантировать порядок их выполнения, вам нужно будет использовать дополнительные стратегии сохранения состояния и дома.