Содержание

basis.event

Модуль basis.event вводит событийную модель. В основе реализации лежит паттерн observer.

basis.event предоставляет единственный класс Emitter и вспомогательные функции.

Emitter

Класс basis.event.Emitter (docs) является предком для большинства классов basisjs.

Инициация события осуществляется вызовом специального метода. Такие методы принято называть с префиксом emit_, а функция метода создается с помощью функции basis.event.create. (см. Событийные методы)

var event = basis.require('basis.event');
var Example = event.Emitter.subclass({
  emit_myEvent: event.create('myEvent') // создание события myEvent
});
var foo = new Example();
foo.emit_myEvent(); // инициация события

Обработка событий

Для того чтобы совершать некоторые действия при возникновении события, объекту добавляются обработчики событий. Обработчик - это plain object, где ключ - название события, а значение - функция, которая должна быть вызвана, когда это событие наступит. Обработчики добавляются методом Emitter#addHandler, при этом также можно указать контекст выполнения для функций обработчика. Если контекст не задан, то контекстом будет объект, чей метод Emitter#addHandler был вызван.

Повторный вызов Emitter#addHandler с теми же самыми аргументами приведет к выводу предупреждения в консоли, так как это, скорее всего, ошибка. Количество добавляемых экземпляру обработчиков не ограничивается.

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

var event = basis.require('basis.event');
var Example = event.Emitter.subclass({
  emit_testEvent: event.create('testEvent'),
  emit_eventWithArgs: event.create('eventWithArgs', 'a', 'b')
});
var foo = new Example({ name: 'foo' });
var bar = new Example({ name: 'bar' });

foo.addHandler({
  testEvent: function(){        // testEvent - название события
    // sender -> foo
    // this -> foo
    console.log('testEvent: event context is', this.name);
  }
});                             // контекст не указан

foo.addHandler({
  testEvent: function(sender){
    // sender -> foo
    // this -> bar
    console.log('testEvent: event context is', this.name);
  },
  eventWithArgs: function(sender, a, b){
    // sender -> foo
    // this -> bar
    console.log('eventWithArgs:', a, b);
  }
}, bar);                        // устанавливаем в качестве контекста bar

foo.emit_testEvent();
// console> testEvent: event context is bar
// console> testEvent: event context is foo

foo.emit_eventWithArgs(1, 2);
// console> eventWithArgs: 1 2

Удаляются обработчики методом Emitter#removeHandler. При вызове Emitter#removeHandler нужно передать те же значения, что и при вызове метода Emitter#addHandler.

// НЕПРАВИЛЬНО
// метод removeHandler не удалит обработчик, так как первые аргументы
// в вызовах foo.addHandler и foo.removeHandler - это два разных объекта
foo.addHandler({
  testEvent: function(){
    console.log('testEvent fired');
  }
}, bar);
...
foo.removeHandler({
  testEvent: function(){
    console.log('testEvent fired');
  }
}, bar);

// ПРАВИЛЬНО
var FOO_HANDLER = {
  testEvent: function(){
    console.log('testEvent fired');
  }
};

foo.addHandler(FOO_HANDLER, bar);
...
foo.removeHandler(FOO_HANDLER, bar);

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

Обработчик по умолчанию

Обработчик можно назначить при создании экземпляра Emitter (или его наследников).

var event = basis.require('basis.event');
var foo = new event.Emitter({
  handler: {                       // обработчик, контекстом будет сам экземпляр
    event1: function(){ .. },
    event2: function(){ .. }
  }
});

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

var event = basis.require('basis.event');
var foo = new event.Emitter({
  handler: {
    context: bar,
    callbacks: {                   // обработчик
      event1: function(){ .. },
      event2: function(){ .. }
    }
  }
});

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

var event = basis.require('basis.event');
var ClassA = event.Emitter.subclass({
  emit_event: event.create('event'),
  eventCount: 0,
  handler: {
    event: function(sender){
      this.eventCount++;
    }
  }
});

var ClassB = ClassA.subclass({
  handler: {  // свойство переопределено, счетчик eventCount не будет изменяться
    ...
  }
});

var ClassC = ClassA.subclass({
  handler: {
    event: function(){
      ClassA.prototype.handler.event.apply(this, agruments);
      ...
    },
    ...
  }
});

В данном случае, вместо назначения handler будет лучше определить логику в методе emit_event.

Событийные методы

Событийные методы создаются с помощью функции basis.event.create, которой передается название события. Такие функции обходят список обработчиков и вызывают функции для конкретного события. Такие функции сохраняются в прототип класса или экземпляр с префиксом emit_, после чего идет имя события.

Кроме того, событийный метод, который возвращает basis.event.create, становится свойством объекта basis.event.events.

Имя события может быть любым, за исключением *.

Ключ * в обработчике используется для назначения функции, которая будет выполняться на любое событие. Такая функция получает объект описывающий событие, где:

  • type - название события;

  • sender - инициатор события (чей emit_* метод был вызван);

  • args - аргументы, с которыми было инициировано событие.

var event = basis.require('basis.event');
var emitter = new event.Emitter({
  emit_customEvent: event.create('customEvent')
});

emitter.addHandler({
  customEvent: function(sender, arg){
    console.log('customEvent', sender, [arg]);
  },
  '*': function(event){
    console.log('*', event.type, event.sender, event.args);
  }
});

emitter.emit_customEvent('test');
// console> customEvent [object basis.event.Emitter] ['test']
// console> '*' customEvent [object basis.event.Emitter] ['test']

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

var Example = basis.data.Object.subclass({
  emit_update: function(delta){
    // действия до обработки обработчиков

    basis.data.Object.prototype.emit_update.call(this, delta);

    // действия после обработки обработчиков
  }
});

Если нужно добавить логику добавляемому событийному методу, то можно воспользоваться слудующей техникой:

var event = basis.require('basis.event');
var Example = event.Emitter.subclass({
  emit_myEvent: event.create('myEvent', 'arg') && function(arg){
    // действия до обработки обработчиков

    event.events.myEvent.call(this, arg);

    // действия после обработки обработчиков
  }
});

Таким образом, в прототип класса Example будет добавлен метод, который оборачивает вызов событийного метода для события myEvent. Обратитесь к разделу событийные методы для более подробной информации.

destroy

Emitter определяет единственное событие destroy, которое выбрасывается при разрушении экземпляра (вызов метода destroy).

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

listen

Класс Emitter определяет расширяемое свойство (basis.Class.nestedExtendProperty), для хранения обработчиков на вложенные объекты (экземпляры Emitter или его наследников). Сам Emitter не использует это свойство, однако его широко используют классы унаследованные от него.

// класс
var event = basis.require('basis.event');
var Foo = event.Emitter.subclass({
  listen: {
    bar: {
      event: function(sender){
        // sender -> foo.bar
        // this -> foo
      }
    }
  },

 /**
  * @type {basis.event.Emitter}
  */
  bar: null,

  setBar: function(bar){
    if (this.bar !== bar)
    {
      if (this.bar && this.listen.bar)
        this.bar.removeHandler(this.listen.bar, this);

      this.bar = bar;

      if (this.bar && this.listen.bar)
        this.bar.addHandler(this.listen.bar, this);
    }
  }
});

// экземпляр
var list = new basis.ui.Node({
  selection: true,
  listen: {
    selection: {
      itemsChanged: function(sender){
        // this -> list
        // sender -> selection
      }
    }
  }
});

Отладка

В dev режиме доступно свойство debug_emit. Данное свойство можно задать как для экземпляра, так и для любого класса (наследника Emitter). Если этому свойству присвоена функция, то эта функция будет вызываться на любое событие после обработки всех обработчиков. Такая функция получает единственный аргумент - объект, который содержит поля:

  • type - название события;

  • serder - инициатор события (чей emit_* метод был вызван);

  • args - аргументы, с которыми было инициировано событие.

basis.data.Object.prototype.debug_emit = function(event){
  console.log(event.type, event.sender, event.args);
};

var foo = new basis.data.Object();
foo.update({ value: 123 });
// console> 'update' [object basis.data.Object] { value: undefined }

Так как обработчики хранятся в виде линейных списков, то не удобно просматривать полный список добавленных обработчиков. Для просмотра списка обработчиков в виде массива можно воспользоваться методом Emitter#debug_handlers. Этот метод доступен только в dev режиме.