События в jQuery: .on(), .off(), делегирование и объект event
Опубликовано: 26.09.2026 · Обновлено: 26.09.2026 · Актуально для jQuery 3.7.1 и 4.0.0
С версии 1.7 в jQuery остался один способ навесить обработчик — .on(), а снять его — .off(). Методы .bind(), .delegate() и .live() — это история: .live() удалён ещё в 1.9, .bind() и .delegate() с версии 3.0 считаются устаревшими, но пока работают и в jQuery 4.0. Если обработчик клика не срабатывает на элементах, добавленных через Ajax или шаблонизатор, — дело почти всегда в отсутствии делегирования: вместо $(".item").on("click", fn) нужно $(document).on("click", ".item", fn), где .item может появиться в DOM позже.
Эволюция API событий
За пятнадцать с лишним лет в jQuery сменилось три поколения методов для работы с событиями. Если вы поддерживаете старый проект, полезно понимать, что перед вами: рабочий, но устаревший код или уже нерабочий после обновления библиотеки.
| Метод | Добавлен | Статус | Замена |
.bind() / .unbind() | 1.0 | Deprecated с 3.0, работает в 4.0 | .on() / .off() |
.live() / .die() | 1.3 | Deprecated в 1.7, удалён в 1.9 | делегированный .on() |
.delegate() / .undelegate() | 1.4.2 | Deprecated с 3.0, работает в 4.0 | .on() с селектором |
.on() / .off() | 1.7 | Актуальный способ | — |
Шорткаты .click(), .change(), .hover() и т. п. (полный список — все события jQuery) | 1.0 | Deprecated с 3.3, работают в 4.0 | .on("click", fn) и т. д. |
Разница между «deprecated» и «удалён» здесь принципиальна. .live() и .die() физически исчезли из кода jQuery в версии 1.9 — вызов бросит TypeError: $(...).live is not a function. А вот .bind(), .delegate() и событийные шорткаты в списке deprecated с 3.0/3.3, но при обновлении на jQuery 4.0.0 в январе 2026 года их не тронули: в разделе «Deprecated APIs removed» апгрейд-гайда к 4.0 перечислены утилиты вроде jQuery.type, jQuery.isArray, jQuery.trim и несколько внутренних методов — событийных методов там нет. Это не гарантия на будущее (в jQuery Migrate 4.x эти вызовы по-прежнему помечаются предупреждением), но прямо сейчас переписывать рабочий .bind() ради самого факта устаревания не обязательно — если только вы не готовите проект к следующему мажорному релизу.
Синтаксис .on()
У .on() два основных варианта вызова.
// Обычная привязка
$(elem).on("click", function (event) {
console.log("клик по", this);
});
// С делегированием — второй аргумент "selector"
$(parent).on("click", ".item", function (event) {
console.log("клик по элементу .item внутри parent");
});
Второй вариант — с картой событий вместо строки и функции: ключи объекта — имена событий, значения — обработчики.
$(elem).on({
click: function (event) {
console.log("click");
},
mouseenter: function (event) {
console.log("mouseenter");
}
});
Несколько событий на один обработчик перечисляются через пробел в одной строке:
$(elem).on("focus blur", function (event) {
console.log(event.type); // "focus" или "blur"
});
Между именем событий и обработчиком можно передать произвольные данные — они попадут в обработчик как event.data, причём ровно те же самые для каждого вызова (не путайте с данными, которые нужно вычислять на каждый клик заново):
$(elem).on("click", { userId: 42 }, function (event) {
console.log(event.data.userId); // 42
});
Внутри обработчика, объявленного через function, this указывает на DOM-элемент, на котором сработал обработчик, — то же самое, что event.currentTarget, только не обёрнутое в jQuery ($(this) даёт jQuery-объект). Если обработчик — стрелочная функция, this берётся из внешнего контекста, а не из события, и внутри неё придётся использовать только event.currentTarget:
// this — DOM-элемент кнопки
$(".btn").on("click", function (event) {
$(this).toggleClass("active");
});
// this здесь НЕ элемент кнопки — это ловушка стрелочных функций
$(".btn").on("click", (event) => {
$(event.currentTarget).toggleClass("active");
});
Делегирование событий
Делегирование решает две задачи: обработчики для элементов, которых ещё нет на странице, и меньше обработчиков вместо тысяч одинаковых. Работает это на всплытии (event bubbling) — большинство DOM-событий поднимается от цели вверх по дереву предков, и jQuery ловит их на общем родителе, а затем сверяет, попадает ли исходная цель под указанный CSS-селектор.
<table id="orders">
<tbody>
<!-- строки добавляются через Ajax -->
</tbody>
</table>
<script>
// Один обработчик на всю таблицу, а не на каждую строку
$("#orders").on("click", "tr.order-row", function (event) {
var orderId = $(this).data("order-id");
console.log("открыть заказ", orderId);
});
// Строки добавляются позже — обработчик всё равно сработает
$.get("/api/orders", function (rows) {
$("#orders tbody").append(rows);
});
</script>
Если после Ajax-запроса клик по новым элементам «не работает» — почти всегда это значит, что обработчик повешен напрямую на сами элементы ($(".order-row").on(...)) до того, как они появились в DOM. Прямая привязка работает только с уже существующими на момент вызова элементами; для будущих нужен делегированный вариант с селектором вторым аргументом у общего, статично существующего родителя.
В обработчике различаются три связанных, но разных элемента:
event.target — самый глубокий элемент, где реально произошло событие (может быть, например, <span> внутри строки таблицы);
event.currentTarget — элемент, на котором jQuery сейчас выполняет обработчик (при делегировании — это найденный по селектору потомок, а не тот, на котором вызван .on());
event.delegateTarget — элемент, к которому обработчик был фактически привязан через .on() (в примере выше — #orders).
Не все события всплывают одинаково удобно для делегирования. focus и blur в браузере не всплывают вовсе — для делегирования нужно подписываться на всплывающие пары focusin/focusout. В jQuery до 4.0 они дополнительно эмулировались (шимились) для старых браузеров, где нативной поддержки не было; в 4.0.0 эту эмуляцию убрали как ненужную для актуальных браузеров — событие работает нативно.
mouseenter и mouseleave тоже не всплывают по спецификации, но их делегированную версию jQuery продолжает эмулировать сама: внутри она вешает на общий обработчик mouseover/mouseout и проверяет event.relatedTarget, чтобы вызвать ваш код только при реальном входе/выходе курсора за границы совпавшего элемента, а не при переходе между его дочерними узлами. Для вашего кода это прозрачно — $(parent).on("mouseenter", ".card", fn) работает так, как ожидается, просто дороже по производительности, чем прямой обработчик, если карточек мало и они статичны.
| Событие | Всплывает нативно | Делегирование через .on() |
| click, keydown, submit, change | да | работает напрямую |
| focus, blur | нет | подписывайтесь на focusin/focusout |
| mouseenter, mouseleave | нет | работает — jQuery эмулирует поверх mouseover/mouseout |
Обратная сторона делегирования — производительность на очень общих селекторах. Официальная документация прямо предупреждает: навешивание множества делегированных обработчиков высоко в дереве документа (например, на document) с иерархическими селекторами (#list li a.link) может заметно замедлить обработку событий на страницах с большим DOM — простые селекторы вида tag#id.class проверяются намного быстрее. Практический вывод: делегируйте от ближайшего статичного контейнера, а не от document «на всякий случай».
Снятие обработчиков: .off()
.off() симметричен .on() и умеет снимать обработчики по-разному:
// Все обработчики click (и прямые, и делегированные) с элемента
$(elem).off("click");
// Только делегированные для конкретного селектора
$(parent).off("click", ".item");
// Только конкретная функция — остальные click остаются
function onItemClick(event) { /* ... */ }
$(parent).on("click", ".item", onItemClick);
$(parent).off("click", ".item", onItemClick);
// Вообще все обработчики любых событий с элемента
$(elem).off();
Чтобы не задеть чужие обработчики на том же элементе и событии, используйте пространства имён — произвольную метку после точки в имени события:
$(document).on("click.myWidget", ".toggle", onToggle);
$(document).on("keydown.myWidget", onEscClose);
// Снять только обработчики своего виджета, не трогая чужие click/keydown
$(document).off(".myWidget");
Это особенно важно в делегированных обработчиках на document или body: там часто «живут» события сразу нескольких независимых частей интерфейса, и снятие без пространства имён снесёт заодно и чужие подписки на то же имя события.
Отдельный источник утечек памяти и дублирующихся обработчиков — разница между .remove() и .detach(). .remove() удаляет элемент из DOM и одновременно чистит все привязанные к нему через jQuery данные и обработчики. .detach() тоже убирает элемент из DOM, но специально оставляет данные и обработчики нетронутыми — это сделано для того, чтобы можно было вставить тот же элемент обратно с уже готовыми обработчиками. Если вы используете .detach() для временного скрытия и потом создаёте новый элемент с нуля вместо повторной вставки старого, обработчики на отсоединённом узле останутся висеть в памяти до сборки мусора — на практике это редко критично, но стоит понимать разницу.
.one(): обработчик на один раз
.one() ведёт себя как .on(), включая делегирование, но сам снимает себя после первого срабатывания на конкретном элементе. Классический пример — защита формы от повторной отправки при двойном клике:
$("#order-form").one("submit", function (event) {
event.preventDefault();
var $form = $(this);
$.post($form.attr("action"), $form.serialize(), function () {
$form.find("button[type=submit]").prop("disabled", true).text("Отправлено");
});
});
Если строка событий содержит несколько типов через пробел, .one() отработает по одному разу на каждый тип, а не один раз суммарно — $(elem).one("mouseenter mouseleave", fn) вызовет fn дважды: один раз при входе курсора, один раз при выходе.
Объект event
В обработчик jQuery всегда передаёт собственный объект jQuery.Event, а не «сырое» браузерное событие — это нормализует различия между браузерами и добавляет пару удобных методов. Оригинальное событие доступно через event.originalEvent.
| Свойство / метод | Что содержит |
type | Имя события, например "click" |
target | DOM-элемент, где событие реально произошло |
currentTarget | Элемент, на котором сейчас выполняется обработчик |
delegateTarget | Элемент, к которому был привязан обработчик через .on() |
relatedTarget | Второй причастный элемент (например, куда ушёл фокус при blur) |
pageX, pageY | Координаты курсора относительно документа |
which | Код клавиши/кнопки мыши — для клавиатуры в jQuery 4.0 без прежней шим-нормализации |
key, code | Нативные строковые обозначения клавиши — jQuery копирует их из originalEvent |
data | Данные, переданные вторым аргументом в .on() |
timeStamp | Время создания события в миллисекундах |
result | Значение, которое вернул последний вызванный обработчик |
originalEvent | Нативный объект события браузера |
preventDefault() | Отменяет действие браузера по умолчанию (переход по ссылке, отправку формы) |
stopPropagation() | Останавливает всплытие события выше по дереву |
stopImmediatePropagation() | Останавливает всплытие и не даёт сработать остальным обработчикам на этом же элементе |
isDefaultPrevented() | true, если уже вызывали preventDefault() |
Про event.which для клавиатуры стоит сказать отдельно. Раньше jQuery дополнительно нормализовал устаревшие keyCode/charCode, приводя их к единому which, чтобы сгладить различия между браузерами. В jQuery 4.0.0 эту часть шима убрали — она была нужна только для браузеров, которые давно не поддерживаются. Для определения нажатой клавиши сейчас надёжнее использовать нативное строковое имя — event.key (например, "Enter", "Escape"): jQuery копирует его (как и event.code) на свой объект события, так что обращаться к originalEvent для этого не нужно.
Фраза «return false из обработчика — это preventDefault + stopPropagation» не метафора: внутри jQuery обрабатывает возврат false именно так — вызывает оба метода за вас. Удобно для одной строчки, но неявно, поэтому в обработчиках со сложной логикой лучше вызывать оба метода явно и только там, где это действительно нужно.
Генерация событий: .trigger() и .triggerHandler()
.trigger() имитирует событие программно — не только вызывает привязанные через jQuery обработчики, но и запускает поведение браузера по умолчанию там, где оно есть: $("form").trigger("submit") действительно отправит форму, $("a").trigger("click") — выполнит переход по ссылке. Если нужно вызвать только обработчики, привязанные через jQuery, без побочных эффектов браузера и без всплытия по дереву, используйте .triggerHandler().
// Своё событие с дополнительными данными
$(document).on("cart:updated", function (event, cart) {
console.log("товаров в корзине:", cart.count);
});
$(document).trigger("cart:updated", [{ count: 3 }]);
// Только обработчики jQuery, без всплытия и без действия браузера
$("#search-form").triggerHandler("submit");
Со стороны нативного JS то же самое пользовательское событие обычно создают через CustomEvent и element.dispatchEvent(). Так как .on() в конечном счёте вешает обработчик через addEventListener, jQuery-обработчик получит и такое, нативно отправленное событие. Данные, которые нативный код кладёт в detail, доступны в обработчике как event.detail — это свойство входит в список, который jQuery проксирует с исходного события (то же значение лежит в event.originalEvent.detail). Обратное неверно: дополнительные аргументы из .trigger("имя", [данные]) приходят в jQuery-обработчик отдельными параметрами, а нативные слушатели addEventListener их не увидят вовсе.
Для отправки формы стандартный обработчик — submit на самой форме, а не click на кнопке (у формы может быть несколько способов отправки, включая Enter в текстовом поле):
$("#login-form").on("submit", function (event) {
event.preventDefault();
// здесь — валидация и отправка через $.ajax/fetch
});
Для отслеживания ввода в текстовые поля есть выбор между change и input. change у текстовых полей срабатывает только при потере фокуса после изменения значения, input — на каждое изменение содержимого, включая вставку из буфера обмена и автозаполнение. Для живой валидации по мере ввода нужен именно input:
$("#email").on("input", function () {
var valid = /\S+@\S+\.\S+/.test(this.value);
$(this).toggleClass("is-invalid", !valid);
});
Для клавиатуры используются keydown (нажатие, повторяется при удержании), keyup (отпускание) и keypress. keypress считается устаревшим на уровне спецификации DOM — он никогда не срабатывал для клавиш без символа (стрелки, Backspace) и в современных браузерах помечен как нежелательный к использованию; для новых обработчиков предпочтительны keydown/keyup в связке с event.key.
Частые ошибки
| Ошибка | Что происходит | Как избежать |
| Повторная привязка при каждом обновлении блока | Обработчики множатся, событие срабатывает 2, 3, N раз | Делегировать один раз от статичного родителя вместо привязки на каждый Ajax-рендер |
Делегирование от document на всё подряд | Каждое событие на странице проходит через лишнюю проверку селектора — заметно на больших DOM | Делегировать от ближайшего статичного контейнера, а не от document |
return false вместо явного preventDefault() | Всплытие незаметно глушится, ломаются делегированные обработчики выше по дереву | Вызывать preventDefault() и stopPropagation() явно и раздельно |
.on() вызван до готовности DOM | На момент вызова элемента ещё нет, обработчик не на что вешать | Оборачивать в $(function () { ... }) или делегировать от элемента, который точно уже есть |
Ожидание passive-слушателей от .on() | jQuery не поддерживает опцию passive в .on() — предложение добавить её обсуждается в issue-трекере jQuery годами и не реализовано | Для touchstart/touchmove/wheel, где важна пассивность, вешать обработчик нативно через addEventListener(type, fn, { passive: true }) |
То же самое на чистом JS
Если часть кода переводится на нативный JavaScript, соответствия такие:
| jQuery | Нативный аналог |
.on("click", fn) | el.addEventListener("click", fn) |
.off("click", fn) | el.removeEventListener("click", fn) |
.one("click", fn) | el.addEventListener("click", fn, { once: true }) |
.on("click", ".item", fn) (делегирование) | parent.addEventListener("click", e => { if (e.target.closest(".item")) fn(e); }) |
.trigger("click") | el.dispatchEvent(new Event("click")) |
Подробный разбор с примерами и нюансами разницы поведения — в статье события на чистом JavaScript. Про остальные изменения в новой версии библиотеки — в статье изменения в jQuery 4.0.
Чек-лист
- Новый код — только через
.on()/.off(); .bind()/.delegate() трогать не обязательно, если они уже работают, но и не размножать в новых местах.
- Обработчик на элементы, которые появляются динамически (Ajax, шаблоны, инфинити-скролл), — всегда делегированный, от ближайшего статичного родителя.
- Повторный рендер блока — не повод вызывать
.on() заново: обработчик либо делегирован один раз, либо снимается через .off() перед повторной привязкой.
- Публичные виджеты и плагины — обработчики только с пространством имён, снятие тоже по нему, а не голым
.off("click").
preventDefault() и stopPropagation() — раздельно и явно, return false — только когда нужны оба эффекта одновременно.
- Клавиатурные обработчики — по
event.key, а не по устаревающей нормализации event.which.
- Прокрутка/тач-события с высокой частотой — нативный
addEventListener с passive: true, jQuery такой режим не поддерживает.
Частые вопросы
Чем .on() отличается от .click()?
.click(fn) — это шорткат, который под капотом вызывает .on("click", fn). Разницы в поведении нет, но с jQuery 3.3 такие событийные шорткаты (.click(), .hover(), .change() и т. д.) помечены как deprecated — в новом коде лучше сразу писать .on("click", fn), это явно показывает, что вы вешаете обработчик, а не эмулируете клик.
Почему обработчик срабатывает два раза?
Чаще всего потому, что .on() вызывается повторно на одном и том же элементе — например, внутри функции, которая перерисовывает блок после каждого Ajax-запроса. Каждый вызов добавляет ещё один обработчик, а не заменяет старый. Решение: делегировать обработчик один раз от статичного родителя, либо перед повторным .on() вызывать .off() с тем же пространством имён.
Как повесить обработчик на элементы, которых ещё нет в DOM?
Через делегирование: подписаться на статичный родительский элемент и указать селектор дочерних элементов вторым аргументом — $(parent).on("click", ".child", handler). jQuery слушает событие на parent и сверяет event.target с селектором на момент всплытия, поэтому сработает даже на элементе, которого не было в DOM в момент вызова .on().
Чем заменить .live()?
.live() удалён в jQuery 1.9, прямой замены в виде функции нет — нужно переписать вызов на делегированный .on(). $(sel).live("click", fn) становится $(document).on("click", sel, fn) (в идеале — не document, а ближайший статичный контейнер).
Как отменить отправку формы в обработчике?
Вызвать event.preventDefault() в обработчике события submit, повешенном через .on("submit", fn). Если нужно отменить отправку и одновременно не дать событию всплыть дальше — вызвать также event.stopPropagation() или использовать return false, который делает и то, и другое сразу.
Что использовать — return false или preventDefault?
return false внутри обработчика jQuery эквивалентен одновременному вызову event.preventDefault() и event.stopPropagation(). Это удобно, но часто отменяет всплытие незаметно для разработчика и ломает делегирование выше по дереву. Явные вызовы понятнее и предсказуемее — используйте return false только тогда, когда нужны оба эффекта сразу.
Работают ли .bind() и .delegate() в jQuery 4?
Да. В jQuery 4.0.0 из объявленных ранее deprecated API убрали в основном утилиты-дублёры нативного JavaScript вроде jQuery.type, jQuery.isArray и jQuery.trim — .bind(), .unbind(), .delegate(), .undelegate() и событийные шорткаты остались рабочими, хотя официально устарели с 3.0/3.3. Единственные реально удалённые из этой группы методы — .live() и .die(), это случилось ещё в 1.9.
[ наверх ]