Declarative event binding / handling

от автора

Всем привет! Хочу показать свой кусочек 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.

Чего нет

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

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

ссылка на оригинал статьи http://habrahabr.ru/post/212883/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *