preventDefault: отмена действия по умолчанию
Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.
У браузера свои привычки: по клику на ссылку он уходит на другой адрес, по кнопке внутри формы — отправляет её, по клику на чекбокс — ставит галочку. preventDefault() — способ сказать «в этот раз не надо» и взять поведение на себя.
Действие по умолчанию — реакция браузера, зашитая в саму разметку. JavaScript для неё не нужен: ссылка ходит, форма отправляется, галочка ставится, потому что так описан HTML. Событие здесь не причина действия, а объявление о нём — браузер сначала рассылает click или submit всем слушателям и только потом делает своё дело.
| Событие | Что браузер делает сам |
|---|---|
| click по ссылке | переход по адресу из href |
| submit у формы | отправка данных и переход на action |
| click по чекбоксу | переключение галочки |
| keydown в поле ввода | вставка символа в текст |
| contextmenu | системное меню правой кнопки |
| wheel над страницей | прокрутка |
Именно этот порядок даёт окно для вмешательства: пока обработчики выполняются, действия ещё не было. С чекбоксом браузер даже слегка жульничает — переключает галочку заранее, чтобы обработчик видел новое состояние, и откатывает обратно, если отмену всё-таки попросили.
Метод вызывают внутри обработчика, и на этом всё. Он ничего не возвращает, не бросает исключений и не прерывает функцию: код после него выполняется как ни в чём не бывало. Отменяется ровно одна вещь — встроенное действие браузера.
Так устроена любая форма, которая отправляет данные сама: отменить, собрать значения, послать fetch. Со ссылками то же самое — на этом стоят клиентские роутеры: клик перехватывают, адрес меняют через History API, страницу перерисовывают без перезагрузки.
И главное, чего не делает preventDefault(): он не останавливает событие. Оно продолжит идти по остальным слушателям и всплывать к родителям — за это отвечает stopPropagation(), совсем другой метод.
Старый приём — вернуть false из обработчика — работает у обработчиков вида on… — и в атрибуте разметки onclick, и при присваивании свойству link.onclick. У слушателя, повешенного через addEventListener, возвращённое значение никто не читает: там либо preventDefault(), либо ничего.
Отмена оставляет след. У события есть булево свойство defaultPrevented: пока никто не звал preventDefault() — false, после вызова — true. Флаг лежит на самом объекте события, а объект один на весь путь, поэтому его видят и следующие слушатели того же элемента, и все родители, до которых событие всплывёт.
Это способ разным слоям кода договориться, не зная друг о друге. Обработчик на контейнере смотрит на флаг и не лезет туда, где уже поработал более конкретный код. Библиотеки ведут себя так же: увидев поднятый флаг, считают событие обслуженным и молча отходят.
Отмена — не право, а разрешение, и дают его не всегда. Первое условие — свойство cancelable. Если оно false, вызов просто ничего не делает: ни ошибки, ни предупреждения, тишина. Такими приходят события-уведомления о свершившемся, например input и change: отменять там нечего, всё уже случилось. Но и true не обещает, что отмене есть что остановить. В Firefox cancelable равен true у scroll, а в Chrome — у transitionend, которым браузер сообщает о конце перехода CSS. defaultPrevented после вызова станет true, но прокрутка уже случилась, а переход уже доехал. Для своих событий удобна проверка через возвращаемое значение dispatchEvent(): false означает «отменили».
Второе условие — слушатель не должен быть пассивным. { passive: true } — обещание браузеру ничего не отменять; взамен браузер не ждёт код и прокручивает страницу сразу. Обещание проверяется: preventDefault() внутри такого слушателя игнорируется. Для touchstart, touchmove и wheel на уровне документа пассивность включена по умолчанию — если прокрутку правда надо отменять, её выключают явно через { passive: false }.
Отменять нужно синхронно, пока событие рассылается. Если обработчик async и preventDefault() оказался после await, браузер давно сделал своё дело. Коварство в том, что флаг всё равно поднимется: defaultPrevented честно скажет true, а галочка при этом будет стоять. Отсюда правило — сначала отменить, потом ждать.
Правило простое: отменяй только то, чему нашёл замену. preventDefault() на форме без собственной отправки — это форма, которая молча ничего не делает. Отмена перехода без своей навигации — ссылка в никуда.
За действием по умолчанию стоят привычки пользователя, и они дороже удобства кода. Перехваченный клик ломает Ctrl+клик и открытие в новой вкладке, если код не пропускает такие клики дальше. Отмена wheel и touchmove ломает прокрутку — главная причина страниц, которые «залипают» на телефоне. Отмена keydown в поле мешает вставке и работе с клавиатуры. Всё это допустимо, но за это надо честно отработать.
Достаточно посмотреть на модификаторы: если нажат Ctrl, Cmd или Shift, пользователь обращается к браузеру, а не к вам. Выходим из обработчика без отмены — и ссылка открывается как обычно.
- Действие по умолчанию — встроенная реакция браузера на событие: переход по ссылке, отправка формы, переключение чекбокса. Событие приходит раньше действия, поэтому у кода есть окно, чтобы вмешаться.
event.preventDefault()отменяет только это встроенное действие. Он не прерывает функцию, не останавливает всплытие и не мешает другим слушателям — за остановку отвечаетstopPropagation().event.defaultPrevented— след отмены на самом объекте события. Его видят все, кто получит событие следом, включая родителей: так разные слои кода договариваются, кто уже отработал.- Отмена не сработает при
cancelable === false, в слушателе с{ passive: true }и послеawait. Исключения не будет — просто ничего не произойдёт, а флаг в последнем случае ещё и соврёт. - Отменяйте только то, что заменяете своим поведением, и оставляйте браузеру клики с модификаторами, прокрутку и клавиатуру.
1/3У чекбокса единственный обработчик click, и он вызывает event.preventDefault(). Что покажет box.checked сразу после box.click()?
2/3На ссылке два обработчика click. Первый вызывает preventDefault(). Что произойдёт со вторым?
3/3Слушатель добавлен как addEventListener("wheel", handler, { passive: true }), и внутри вызывается preventDefault(). Что будет?
Форма, которая остаётся на странице
В разметке форма поиска. Повесьте обработчик submit, отмените отправку и выведите значение поля q через FormData. Затем запустите отправку из кода — form.requestSubmit() ведёт себя как настоящее нажатие кнопки.
Отменяем событие первым делом, пока браузер ещё ждёт, и только потом читаем данные.
Без preventDefault() браузер отправил бы форму и перезагрузил страницу — вывод исчез бы вместе с ней. Обратите внимание на requestSubmit(): он, в отличие от form.submit(), сначала проверяет валидность и порождает событие submit. Если бы у поля стоял required и оно было пустым, обработчик просто не вызвался бы.
Не отменяй дважды
У первой ссылки уже есть свой обработчик с отменой. Напишите обработчик на контейнере, который берёт переход на себя только тогда, когда действие ещё никто не отменил, а иначе отходит в сторону. Проверьте на обеих ссылках.
Флаг defaultPrevented проверяем до того, как что-то делать: событие всплыло от ссылки к контейнеру уже с поднятым флагом.
Так пишут обработчики на контейнерах и в библиотеках: более конкретный код имеет приоритет, а общий не мешает ему работать.