Declarative event binding / handling

в 8:55, , рубрики: coffeescript, Events, javascript, метки: , ,

Всем привет! Хочу показать свой кусочек CoffeeScript для декларативной подписки и обработки событий.

Предистория

У меня 5 лет инженерного опыта, включающего в себя .NET (+forms, +WPF, +.NET MVC), Java (+Swing, +Tapestry5, +Groovy), JavaScript (+CoffeeScript, +Node).

Последний год я активно пишу собственное одностраничное веб-приложение работающее без перезагрузки, (о котором речь будет в следующих выпусках). Вся динамическая часть пользовательского интерфейса создается на клиенте, с сервера приходят только данные. Как часто бывает в UI, я имею дело с деревом компонентов. Разумеется, для организации взаимодействия дерева мне нужен механизм отправки и обработки событий. Я решил написать свой, и не использовать Backbone, или что-то из Google Closure. В любом случае у меня был опыт реализации этюда Слушатель (Listener pattern).

Класс ИспускательСобытий первой версии если ты хотел слушать его — просто записывал тебя в массив слушателей. Когда возникало событие «Ч» он обходил массив, и искал слушателей с методом «наЧ», и вызывал его. Типа как в Swing / .NET.

Все отлично работало для небольшого числа Испускателей. С ростом системы пришла проблема перекрытия имен событий между разными Испускателями. Подписчик выполнял один и тот же метод «наЧ», даже если «Ч» исходило от разных Испускателей. Затем был более привычный для JS, EventEmitter, как в jQuery / NodeJS. Не буду особо затягивать…

SuperEmitter

Теперь я работаю с табличным представлением дерева «испускатель -> события -> реакции». Пример:

class Brain extends SuperEmitter
  event_table: [
    # источник    событие               реакции
    [ 'ear' , [ [ 'snake_heard'     , [ 'emit_adrenaline'
                                        'look_around'        ] ] ] ]
    [ 'eye' , [ [ 'food_spotted'    , [ 'emit_noradrenaline'
                                        'hunt'
                                        'emit_endorphins'    ] ]
                [ 'predator_spotted', [ 'emit_cortisol'
                                        'emit_adrenaline'
                                        'run'                ] ] ] ]
    [ 'nose', [ [ 'food_smelled'    , [ 'look_around'        ] ]
                [ 'blood_smelled'   , [ 'emit_adrenaline'
                                        'look_around'        ] ] ] ]
  ]

  # constructor and instance members
  constructor = ->
    @ear  = new Ear()
    @eye  = new Eye()
    @nose = new Nose()

  # methods:
  emit_adrenaline: ->
  emit_cortisol: ->
  emit_endorphins: ->
  hunt: ->
  look_around: ->

new Brain().bind_events()

Вместо:

class Brain
  bind_events: ->
    @ear.on 'snake_heard', (args...) => # условные аргументы
      @emit_adrenaline(args...)         # условная передача аргументов
      @look_around(args...)
    @eye.on 'food_spotted', (args...) =>
      @emit_noradrenaline(args...)
      @hunt(args...)
      @emit_endorphins(args...)
    @eye.on 'predator_spotted', (args...) =>
      @emit_cortisol(args...)
      @emit_adrenaline(args...)
      @run(args...)
    @nose.on 'food_smelled', (args...) =>
      @look_around(args...)
    @nose.on 'blood_smelled', (args...) =>
      @emit_adrenaline(args...)
      @look_around(args...)
    # и т.д.

Это моя библиотека ognivo/super-emitter. Она позволяет убрать весь соединительный код, и сосредоточиться на источниках, событиях и реакциях. Кода меньше, он чище, более гранулирован. Формат позволяет видеть, что и откуда происходит и последовательность ответов.
Более того, таблица — это данные, и с ней можно делать все, что можно делать с данными. Сравнивать, склеивать, клонировать и проч.

Демо

Демо онлайн. Это из папки demo в репо. Можете клонировать репо, можете выполнить

npm install super-emitter

. Я хочу сделать более масштабное демо, но пока туго со временем.

Выпуск и прием событий

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

brain = new Brain()
brain.bind_events()
brain.emit('adrenaline')
brain.emit('adrenaline', [dosage_ml = 300, noradrenaline = true]) 

brain.on 'adrenaline', (dosage_ml, noradrenaline) ->
  console.log "I'm running from something"

# OR methods
class Brain extends SuperEmitter
  emit_adrenaline: (dosage_ml = 10, noradrenaline = true) ->
    @emit('adrenaline, [dosage_ml, noradrenaline])
  receive_adrenaline: (dosage, noradrenaline) ->
    if noradrenaline
      console.log "I will fight to death"
    else
      console.log "Let's get outta here"

Требования

Экземпляр на который подписываешься должен иметь метод «on» с сигнатурой (есть ли русское слово?) event_name, callback. Подходят jQuery обертки, KineticJS фигуры (shapes), экземпляры класса SuperEmitter.

Стандартный для DOM-компонентов метод addEventListener имеет такую же сигнатуру, так что с ним тоже ничего сложного. Однажды мне нужно было подписаться на popstate от окна, соответственно все решилось window.on = window.addEventListener.

Чего нет

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

Буду рад критике, похвале и дополнениям.
З.Ы.: я намерено выбрал слово испускатель, а не источник.

Автор: overmind0

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js