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