Блог разработчика 1С-Битрикс

Кастомные события в 1С-Битрикс: как правильно отписываться с BX.removeCustomEvent

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

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

Кастомные события. 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);
      });
    })();
    

Типовые ошибки и как их избежать

  1. Передаёте другую функцию при отписке
    Решение: храните ссылку на обработчик в переменной/поле класса.
  2. Путаете «глобальный» и «объектный» вариант
    Если подписались как BX.addCustomEvent(obj, 'Name', fn), отписываться нужно тем же obj:
    BX.removeCustomEvent(obj, 'Name', fn).
  3. Переинициализируете виджет — подписки дублируются
    В init() сначала снимайте старые подписки или используйте паттерн destroy().
  4. Анонимные функции
    Нельзя снять. Используйте именованные/сохраняемые колбэки.

Рецепты на каждый день

Отписаться от всех обработчиков конкретного события, если вы их регистрировали в одном месте


    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-перезагрузок.

Теги:  кастомные события, BX.removeCustomEvent, фронтенд, JavaScript, обработчики событий


Стоимость услуг по разработке и сопровождению сайтов на 1C-Битрикс

Участие в проекте

привлечение в проект на part-time основе

от 20 000 рублей / неделя

Возможно участие в проекте на ежедневной основе, как разработчика. Занятость - до 20 часов в неделю
Минимальный срок - одна неделя.

* сумма фиксированная

Разработка интернет-магазина с готовой версткой

от 4 недель

от 90 000 рублей

* указана минимальная стоимость. Стоимость выбранной лицензии «1С-Битрикс» оплачивается отдельно.

Техническая поддержка

выполняется с сайтами на основе любых CMS

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

* стоимость зависит от объема и сложности выполняемых работ, используемой CMS.