Кастомные события — один из самых простых и удобных способов «склеивать» независимые куски фронтенда в 1С-Битрикс. Вы подписываетесь на событие где-то в одном месте, генерируете его в другом — и код остаётся слабо связанным.
В этой статье разберём BX.removeCustomEvent
: когда её вызывать, какие есть варианты вызова, типичные ошибки и рабочие паттерны. Будет много живых примеров, которые можно брать и вставлять в проект.

Коротко о сигнатуре
BX.removeCustomEvent
поддерживает два варианта параметров:
// Вариант 1: когда событие «приписано» к объекту-эмиттеру
BX.removeCustomEvent(
/* Object */ eventObject,
/* string */ eventName,
/* Function */ eventHandler
);
// Вариант 2: когда событие «глобальное» (без конкретного объекта)
BX.removeCustomEvent(
/* string */ eventName,
/* Function */ eventHandler
);
Оба варианта ничего не возвращают (void
) и просто «отцепляют» обработчик eventHandler
от события eventName
.
Важно: отписка возможна только если у вас есть та же самая функция, что вы передавали в
BX.addCustomEvent
. Анонимные функции отписать нельзя — ниже покажу, как это обходить.
Базовый цикл: подписка → вызов → отписка
1) Глобальное событие
<div id="global-demo"></div>
<script>
;(function() {
// 1) Создаём именованный обработчик и сохраняем ссылку
function onWidgetReady(params) {
const node = document.getElementById('global-demo');
node.textContent = 'Глобальный onWidgetReady: ' + JSON.stringify(params);
}
// 2) Подписываемся
BX.addCustomEvent('Widget:Ready', onWidgetReady);
// 3) Где-то в коде триггерим событие
setTimeout(function () {
BX.onCustomEvent('Widget:Ready', [{ id: 42, status: 'ok' }]);
}, 300);
// 4) Позже — отписываемся
setTimeout(function () {
BX.removeCustomEvent('Widget:Ready', onWidgetReady);
}, 1500);
})();
</script>
2) Событие, привязанное к объекту
<div id="object-demo"></div>
<script>
;(function() {
// Допустим, у нас есть объект-эмиттер (любой JS-объект)
var emitter = { name: 'LocalEmitter' };
function logOnSave(params) {
document.getElementById('object-demo').textContent =
'Объектное событие Save: ' + JSON.stringify(params);
}
// Подписались на событие, прикреплённое к emitter
BX.addCustomEvent(emitter, 'Save', logOnSave);
// Вызвали (важно: тот же emitter!)
BX.onCustomEvent(emitter, 'Save', [{ changed: ['title', 'price'] }]);
// Отписались
BX.removeCustomEvent(emitter, 'Save', logOnSave);
})();
</script>
Нельзя отписать анонимную функцию. Что делать?
Плохо (так нельзя отписать):
BX.addCustomEvent('Cart:Change', function (params) {
console.log('Cart changed', params);
});
// ... а теперь как отписаться? Ссылки на функцию нет — никак.
Хорошо (храним ссылку):
var handleCartChange = function (params) {
console.log('Cart changed', params);
};
BX.addCustomEvent('Cart:Change', handleCartChange);
// ... позже
BX.removeCustomEvent('Cart:Change', handleCartChange);
Одноразовый обработчик (once
) без сторонних библиотек
Иногда нужно, чтобы обработчик отработал ровно один раз. Сделаем мини-обёртку, которая сама отцепится после первого вызова:
function once(eventTargetOrName, eventNameOrHandler, maybeHandler) {
var useObjectForm = typeof eventTargetOrName === 'object';
var eventName = useObjectForm ? eventNameOrHandler : eventTargetOrName;
var handler = useObjectForm ? maybeHandler : eventNameOrHandler;
var target = useObjectForm ? eventTargetOrName : null;
function wrapper(params) {
try { handler(params); }
finally {
if (target) {
BX.removeCustomEvent(target, eventName, wrapper);
} else {
BX.removeCustomEvent(eventName, wrapper);
}
}
}
if (target) {
BX.addCustomEvent(target, eventName, wrapper);
} else {
BX.addCustomEvent(eventName, wrapper);
}
return wrapper; // вернём, если захотите отписаться вручную раньше
}
// Пример: сработает один раз
once('Uploader:Ready', function (info) {
console.log('Uploader готов', info);
});
Паттерн «виджет/класс» с методами init()
и destroy()
Удобно инкапсулировать подписки и отписки внутри класса. Так вы избежите утечек памяти при смене страниц/вкладок/всплывающих окон.
(function () {
function ProductWidget(options) {
this.node = options.node;
this._handlers = [];
}
ProductWidget.prototype.init = function () {
// 1) Глобальные события
this._onPriceUpdate = this._onPriceUpdate.bind(this);
BX.addCustomEvent('Catalog:PriceUpdate', this._onPriceUpdate);
this._handlers.push(['Catalog:PriceUpdate', this._onPriceUpdate]);
// 2) Объектные события (локальный эмиттер)
this.emitter = { scope: 'ProductWidget' };
this._onLocalSelect = this._onLocalSelect.bind(this);
BX.addCustomEvent(this.emitter, 'Select', this._onLocalSelect);
this._handlers.push([this.emitter, 'Select', this._onLocalSelect]);
// Имитация входящего события
var _this = this;
setTimeout(function () {
BX.onCustomEvent('Catalog:PriceUpdate', [{ id: 7, price: 1990 }]);
BX.onCustomEvent(_this.emitter, 'Select', [{ id: 7 }]);
}, 200);
};
ProductWidget.prototype._onPriceUpdate = function (params) {
this.node.textContent = 'Новая цена: ' + params[0].price + ' ₽';
};
ProductWidget.prototype._onLocalSelect = function (params) {
this.node.dataset.selectedId = String(params[0].id);
};
ProductWidget.prototype.destroy = function () {
// Снимаем все подписки надёжно
for (var i = 0; i < this._handlers.length; i++) {
var entry = this._handlers[i];
if (typeof entry[0] === 'string') {
BX.removeCustomEvent(entry[0], entry[1]);
} else {
BX.removeCustomEvent(entry[0], entry[1], entry[2]);
}
}
this._handlers.length = 0;
this.node.textContent = '';
};
// Демонстрация
var w = new ProductWidget({ node: document.getElementById('product-widget') });
w.init();
// Через 2 секунды демонтируем виджет
setTimeout(function () { w.destroy(); }, 2000);
})();
…
«Снятие» обработчика изнутри (самоотписка при условии)
Отписаться можно прямо внутри обработчика, например, когда интересующее состояние достигнуто:
function onGridLoadedOnce(params) {
try {
if (params && params[0] && params[0].rowsCount > 0) {
console.log('Грид инициализирован с данными');
}
} finally {
BX.removeCustomEvent('MainGrid:Load', onGridLoadedOnce);
}
}
BX.addCustomEvent('MainGrid:Load', onGridLoadedOnce);
Массовая отписка с «реестром» подписок
Если подписок много, удобно вести централизованный реестр и уметь его чистить:
var EventRegistry = (function () {
var stack = [];
function on(targetOrName, nameOrHandler, maybeHandler) {
var isObject = typeof targetOrName === 'object';
var name = isObject ? nameOrHandler : targetOrName;
var handler = isObject ? maybeHandler : nameOrHandler;
var target = isObject ? targetOrName : null;
if (target) BX.addCustomEvent(target, name, handler);
else BX.addCustomEvent(name, handler);
stack.push([target, name, handler]);
return handler;
}
function off(targetOrName, nameOrHandler, maybeHandler) {
var isObject = typeof targetOrName === 'object';
var name = isObject ? nameOrHandler : targetOrName;
var handler = isObject ? maybeHandler : nameOrHandler;
var target = isObject ? targetOrName : null;
if (target) BX.removeCustomEvent(target, name, handler);
else BX.removeCustomEvent(name, handler);
}
function clearAll() {
while (stack.length) {
var rec = stack.pop();
if (rec[0]) BX.removeCustomEvent(rec[0], rec[1], rec[2]);
else BX.removeCustomEvent(rec[1], rec[2]);
}
}
return { on: on, off: off, clearAll: clearAll };
})();
// Пример использования
var handlerA = function (p) { console.log('A', p); };
var handlerB = function (p) { console.log('B', p); };
var obj = { tag: 'X' };
EventRegistry.on('App:Init', handlerA);
EventRegistry.on(obj, 'Tick', handlerB);
// ... потом
EventRegistry.clearAll();
Пример интеграции с AJAX: отписываемся при уходе со страницы
Когда живёте в SPA-подобном сценарии (компонент подгружает себя AJAX-ом), очень легко оставить «висящие» обработчики. Используем beforeunload
и снимем подписки:
(function () {
function onAjaxSuccess(data) {
// Реакция на ваш внутренний AJAX-ивент
console.log('AJAX OK', data);
}
BX.addCustomEvent('MyAjax:Success', onAjaxSuccess);
// Ваш AJAX-триггер
function fakeAjax() {
setTimeout(function () {
BX.onCustomEvent('MyAjax:Success', [{ code: 200, payload: { ok: true } }]);
}, 500);
}
fakeAjax();
// Перед уходом — отписка
window.addEventListener('beforeunload', function () {
BX.removeCustomEvent('MyAjax:Success', onAjaxSuccess);
});
})();
Типовые ошибки и как их избежать
- Передаёте другую функцию при отписке
Решение: храните ссылку на обработчик в переменной/поле класса. - Путаете «глобальный» и «объектный» вариант
Если подписались какBX.addCustomEvent(obj, 'Name', fn)
, отписываться нужно тем же obj:
BX.removeCustomEvent(obj, 'Name', fn)
. - Переинициализируете виджет — подписки дублируются
Вinit()
сначала снимайте старые подписки или используйте паттернdestroy()
. - Анонимные функции
Нельзя снять. Используйте именованные/сохраняемые колбэки.
Рецепты на каждый день
Отписаться от всех обработчиков конкретного события, если вы их регистрировали в одном месте
var callbacks = [];
function subscribeAll() {
var a = function (p) { console.log('A', p); };
var b = function (p) { console.log('B', p); };
BX.addCustomEvent('App:Ping', a);
BX.addCustomEvent('App:Ping', b);
callbacks.push(a, b);
}
function unsubscribeAll() {
for (var i = 0; i < callbacks.length; i++) {
BX.removeCustomEvent('App:Ping', callbacks[i]);
}
callbacks.length = 0;
}
Делегирование контекста (this
) внутри обработчика
function Component(node) {
this.node = node;
this._onPing = this._onPing.bind(this);
BX.addCustomEvent('App:Ping', this._onPing);
}
Component.prototype._onPing = function (p) {
this.node.textContent = 'Ping: ' + JSON.stringify(p);
};
Component.prototype.destroy = function () {
BX.removeCustomEvent('App:Ping', this._onPing);
};
Отписка по таймеру (TTL для обработчика)
function withTTL(eventName, handler, ms) {
BX.addCustomEvent(eventName, handler);
var timer = setTimeout(function () {
BX.removeCustomEvent(eventName, handler);
}, ms);
return function cancel() {
clearTimeout(timer);
BX.removeCustomEvent(eventName, handler);
};
}
var cancel = withTTL('Cache:Warmup', function () {
console.log('Греем кеш...');
}, 3000);
// При необходимости: cancel();
Мини-чек-лист перед релизом
- [ ] Для каждого
BX.addCustomEvent
есть соответствующийBX.removeCustomEvent
. - [ ] Нет анонимных функций-обработчиков.
- [ ] Старые подписки снимаются при ре-инициализации/демонтаже.
- [ ] Для «объектных» событий отписка использует тот же объект, что и подписка.
- [ ] Долгоживущие страницы/попапы очищают подписки по закрытию.
Итог
BX.removeCustomEvent
— важный инструмент здоровья фронтенда на Битриксе. Держите ссылки на обработчики, чётко различайте глобальные и объектные события и закрывайте подписки там же, где открыли. Так вы избежите «призрачных» вызовов, утечек и странных багов после AJAX-перезагрузок.