Matreshka.js: события
Документация на русском
Github репозиторий
Всем привет!
Функционал событий в Матрешке стал настолько богатым, что он, без сомнения, заслужил отдельной статьи. Коротко об изменениях в новой версии я рассказывал в предыдущем посте. Теперь давайте разберемся подробно, почему Матрешка называется event-driven фрейморком.
Небольшой код для привлечения внимания:
var object = {a: {b: {c: {d: 1}}}};
MK.on(object, 'a.b.c@change:d', function(evt) {
alert('"d" изменилось на ' + evt.value);
});
object.a.b.c.d = 2; // "d" изменилось на 2
object.a.b = {c: { d: 42 }}; // "d" изменилось на 42
Напомню, возможности, описанные в этом посте, доступны, в том числе, в виде небольшой библиотеки MatreshkaMagic, на случай, если вам не нужен «фреймворковый» функционал.
Начнем с самого простого. события в Матрешке добавляются методом on.
var handler = function() {
alert('произошло событие "someeevent"');
};
this.on('someevent', handler);
В который можно передать список событий, разделенных пробелами.
this.on('someevent1 someevent2', handler);
Для объявления обработчика события в произвольном объекта (являющегося или не являющегося экземпляром Матрешки), используется статичный метод MK.on (разница только в том, что целевой объект — первый аргумент, а не this
).
var object = {};
MK.on(object, 'someevent', handler);
События можно генерировать методом trigger.
this.trigger('someevent');
Для произвольных объектов можно воспользоваться статичным аналогом метода.
MK.trigger(object, 'someevent');
При этом, можно передать какие-нибудь данные в обработчик, указав первый и последующие аргументы.
this.on('someevent', function(a, b, c) {
alert([a,b,c]); // 1,2,3
});
this.trigger('someevent', 1, 2, 3);
Или
MK.on(object, 'someevent', function(a, b, c) {
alert([a,b,c]); // 1,2,3
})
MK.trigger(object, 'someevent', 1, 2, 3);
Здесь вы можете углядеть синтаксис Backbone. Всё верно: первые строки кода Матрешки писались под (негативным) впечатлением от Backbone (даже код изначально был позаимствован оттуда, хотя и перетерпел большие изменения в дальнейшем).
Дальше, в этом посте, буду приводить вариант методов, использующих ключевое слово this
(за исключением примеров делегированных событий). Просто помните, что on, once, onDebounce, trigger, set, bindNode и прочие методы Матрешки имеют статичные аналоги, принимаемые произвольный целевой объект в качестве первого аргумента.
Кроме метода on
, есть еще два: once
и onDebounce
. Первый навешивает обработчик, который может быть вызван только однажды.
this.once('someevent', function() {
alert('yep');
});
this.trigger('someevent'); // yep
this.trigger('someevent'); // ничего
Второй «устраняет дребезжание» обработчика. Когда срабатывает событие, запускается таймер с заданной программистом задержкой. Если по истечению таймера не вызвано событие с таким же именем, запускается обработчик. Если событие сработало перед окончанием задержки, таймер обновляется и снова ждет. Это реализация очень популярного микропаттерна debounce, о котором можно прочесть на Хабре, на зарубежном ресурсе или на сайте Матрешки.
this.onDebounce('someevent', function() {
alert('yep');
});
for(var i = 0; i < 1000; i++) {
this.trigger('someevent');
}
// через минимальный промежуток времени один раз покажет 'yep'
Не забывайте, что метод может принимать задержку.
this.onDebounce('someevent', handler, 1000);
Когда свойство меняется, Матрешка генерирует обытие: change:KEY
.
this.on('change:x', function() {
alert('x is changed');
});
this.x = 42;
"change:KEY"
, Матрешка генерирует событие "change"
, которое говорит о том, что любое из заявленных свойств изменено. Что такое заявленное (или т. н. «специальное») свойство? При вызове любых методов, требующих прослушивания изменений в том или ином свойстве, запускается псевдо-приватный статичный метод _defineSpecial(object, key)
, навешивающий необходимый акцессор, позволяющий слушать изменения заданного свойства. Разработчики, знакомые с работой Object.defineProperty прекрасно знают, что невозможно навешать акцессор на все свойства сразу. Это значит, что до того, как свойство стало специальным, его изменения невозможно отловить.
this.on('change', function(evt) {
alert(evt.key + ' is changed');
});
Возможно, в будущем Матрешка будет базироваться на Proxy. Но сейчас такой возможности нет из-за отсутствия достойной поддержки браузерами.
Dirty-checking и Object.observe продемонстрировали себя не с лучшей стороны с точки зрения производительности.
Для того, чтоб не путаться в этих особенностях, используйте Matreshka.Object, API которого позволяет указать какие ключи отвечают за данные и отлавливать событие "modify"
, прослушивающее все такие ключи.
Если вы считаете, что объявление специальных свойств должно быть доступно разработчику, голосуйте за соответствующую фичу в Trello.
В случае, если вы хотите передать какую-нибудь информацию в обработчик события или же изменить значение свойства не вызывая при этом события "change:KEY"
, вместо обычного присваивания воспользуйтесь методом Matreshka#set (или статичным методом Matreshka.set), принимающим три аргумента: ключ, значение и объект с данными или флагами.
this.on('change:x', function(evt) {
alert(evt.someData);
});
this.set('x', 42, {someData: 'foo'});
А вот как можно изменить свойство, не вызывая обработчик события:
this.set('x', 9000, {silent: true}); // изменение не генерирует событие
Метод set
поддерживает еще несколько флагов, описание которых заставило бы выйти за рамки темы статьи, поэтому, прошу обратиться к документации к методу.
В версии 1.1 появилась еще одно событие: "beforechange:KEY"
, генерирующееся перед изменением свойства. Событие может быть полезно в случаях, когда вы определяете событие "change:KEY"
и хотите вызвать код, предшествующий этому событию.
this.on('beforechange:x', funtion() {
alert('x will be changed in few microseconds');
});
"change"
, Матрешка включает в себя событие "beforechange"
, позволяющее навешать обработчик предшествующий изменению любого заявленного или «специального» свойства.
this.on('beforechange', funtion() {
alert(evt.key + ' will be changed in few microseconds');
});
В обработчик можно передать какие-нибудь данные или отменить генерацию события.
this.on('beforechange:x', function(evt) {
alert(evt.someData);
});
this.set('x', 42, {someData: 'foo'});
this.set('x', 9000, {silent: true}); // изменение не генерирует событие
При удалении свойств методом remove, генерируются события delete:KEY
и delete
.
this.on('delete:x', function() {
alert('x is deleted');
});
this.on('delete', function(evt) {
alert(evt.key + ' is deleted');
});
this.remove('x');
При объявлении привязки генерируется два события: "bind"
и "bind:KEY"
, где KEY — ключ связанного свойства.
this.on('bind:x', function() {
alert('x is bound');
});
this.on('bind', function(evt) {
alert(evt.key + ' is bound');
});
this.bindNode('x', '.my-node');
Это событие может быть полезно, например, тогда, когда байндинги контролирует другой класс, и вам нужно запустить свой код после какой-нибудь привязки (например, привязки песочницы).
Именно. Когда добавляется событие, генерируются события "addevent"
и "addevent:NAME"
, где NAME — имя события.
this.on('addevent', handler);
this.on('addevent:someevent', handler);
Изначально, событие "addevent:NAME"
было реализовано для внутренних оптимизаций, но было решено вынести его в публичный API.
Одним из способов применения можно назвать использование событий Матрешки в связке с движком событий сторонней библиотеки. Скажем, вы хотите разместить все обработчики для класса в одном единственном вызове on, сделав код читабельнее и комапактнее. С помощью "addevent"
вы перехватываете все последующие инициализации событий, а в обработчике проверяете имя события на соответствие каким-нибудь условиям и инициализируете событие, используя API сторонней библиотеки. В примере ниже код из проекта на ES6, который юзает Fabric.js. Обработчик "addevent"
проверяет имя события на наличие префикса "fabric:"
и, если проверка пройдена, добавляет холсту соответствующий обработчик с помощью Fabric API.
this.canvas = new fabric.Canvas(node);
this.on({
'addevent': evt => {
let {name, callback} = evt,
prefix = 'fabric:';
if(name.indexOf(prefix) == 0) {
name = name.slice(prefix.length);
// навешиваем событие на холст
this.canvas.on(name, callback);
}
},
'fabric:after:render': evt => this.data = this.canvas.toObject(),
'fabric:object:selected': evt => ...
}
Для полноты информации, следует упомянуть, что ядро Матрешки тоже использует события для реализации крутых штук. При изменении свойства, например, генерируется два события: "_runbindings:KEY"
, вызывающий механизм синхронизации с DOM элементом и "_rundependencies:KEY"
, вызывающее механизм ссылок.
Теперь приступим к самому интересному: к делегированием событий. Синтаксис делегированных событий таков: PATH@EVENT_NAME
, где PATH — это путь (свойства разделены точкой) к объекту, на который навешивается событие EVENT_NAME. Давайте разберемся на примерах.
Пример 1
Вы хотите навешать обработчик события в свойстве "a"
, которое является объектом.
this.on('a@someevent', handler);
Обработчик будет вызван тогда, когда в "a"
произошло событие «someevent».
this.a.trigger('someevent'); // если a - экземпляр Матрешки
MK.trigger(this.a, 'someevent'); // если a - обычный объект или экземпляр Матрешки
При этом, обработчик можно объявить и до того, как свойство "a"
объявлено. Если свойство "a"
перезаписать другим объектом, внутренний механизм Матрешки отловит это изменение, удалит обработчик у предыдущего значения свойства и навешает новому значению.
this.a = new MK();
this.a.trigger('someevent');
//или
this.a = {};
MK.trigger(this.a, 'someevent');
Обработчик handler
снова будет вызван.
Пример 2
А что если наш объект — коллекция, унаследованная от MK.Array или MK.Object (MK.Object
— это коллекция, типа ключ-значение, у экземпляров этого класса есть метод each, перебирающий все свойства, отвечающие за данные и поддержка циклов for..of)? Мы заранее не знаем, в каком элементе коллекции произойдет событие (в первом или десятом). Поэтому, вместо имени свойства, для этих классов, можно использовать звездочку "*", говорящую о том, что обработчик события должен вызываться тогда, когда событие вызвано на одном из входящих в коллекцию элементов.
this.on('*@someevent', handler);
Если входящий элемент — экземпляр Матрешки:
this.push(new Matreshka());
this[0].trigger('someevent');
Или, в случае, если входящий элемент либо обычный объект либо экземпляр Матрешки:
this.push({});
MK.trigger(this[0], 'someevent');
Пример 3
Идем глубже. Скажем, у нас есть свойство "a"
, которое содержит объект со свойством "b"
, в котором должно произойти событие "someevent"
. В этом случае, свойства разделаются точкой:
this.on('a.b@someevent', handler);
this.a.b.trigger('someevent');
//или
MK.trigger(this.a.b, 'someevent');
Пример 4
У нас есть свойство "a"
, которое является коллекцией. Мы хотим отловить событие "someevent"
, которое должно возникнуть у какого-нибудь элемента входящего в эту коллекцию. Совмещаем примеры (2) и (3).
this.on('a.*@someevent', handler);
this.a[0].trigger('someevent');
//или
MK.trigger(this.a[0], 'someevent');
Пример 5
У нас есть коллекция объектов, содержащих свойство "a"
, являющееся объектом. Мы хотим навешать обработчик, на все объекты, содержащиеся под ключем "a"
у каждого элемента коллекции:
this.on('*.a@someevent', handler);
this[0].a.trigger('someevent');
//или
MK.trigger(this[0].a, 'someevent');
Пример 6
У нас есть коллекция, элементы которой содержат свойство "a"
, являющееся коллекцией. В свою очередь, последняя включает в себя элементы, содержащие свойство "b"
, являющееся объектом. Мы хотим отловить "someevent"
у всех объектов "b"
:
this.on('*.a.*.b@someevent', handler);
this[0].a[0].b.trigger('someevent');
//или
MK.trigger(this[0].a[0].b, 'someevent');
Пример 7. Различные комбинации
Кроме произвольных событий, можно использовать и встроенные в Матрешку. Вместо "someevent"
можно воспользоваться событием "change:KEY"
, описанное выше или "modify"
, которое позволяет слушать любые изменения в MK.Object
и MK.Array
.
// в объекте "a" есть объект "b", в котором мы слушаем изменения свойства "c".
this.on('a.b@change:c', handler);
// объект "a" - коллекция коллекций
// мы хотим отловить изменения (добавление/удаление/пересортировку элементов) последних.
this.on('a.*@modify', handler);
Напоминаю, что делегированные события навешиваются динамически. При объявлении обработчика, любая ветвь пути может отсутствовать. Если что-то в дереве объектов переопределено, связь со старым значением разрывается и создается связь с новым значением:
this.on('a.b.c.d@someevent', handler);
this.a.b = {c: {d: {}}};
MK.trigger(this.a.b.c.d, 'someevent');
Как известно, Матрешка позволяет связать DOM элемент на странице с каким-нибудь свойством экземпляра Матрешки или обычного объекта, реализуя одно или двух-стороннее связывание:
this.bindNode('x', '.my-node');
//или
MK.bindNode(object, 'x', '.my-node');
Подробнее о методе bindNode.
До или после объявления привязки можно создать обработчик, слушающий DOM события привязанного элемента. Синтаксис таков: DOM_EVENT::KEY
, где DOM_EVENT
— DOM или jQuery событие, а KEY
— ключ привязанного свойства. DOM_EVENT
и KEY
разделены двойным двоеточием.
this.on('click::x', function(evt) {
evt.preventDefault();
});
В объект оригинального DOM события находится под ключём domEvent
объекта события, переданного в обработчик. Кроме этого, в объекте доступно несколько свойств и методов, для того чтобы не обращаться каждый раз к domEvent
: preventDefault
, stopPropagation
, which
, target
и несколько других свойств (key
, node
, $nodes
, self
).
Эта возможность — синтаксический сахар, над обычными DOM и jQuery событиями а код ниже делает то же самое, что и предыдущий:
$('.my-node').on('click', function(evt) {
evt.preventDefault();
});
Объявление событий из примера выше требует объявления привязки. Вы должны совершить два шага: вызвать метод bindNode
и, собственно, объявить событие. Это не всегда удобно, так как часто бывают случаи, когда DOM узел нигде не используется, кроме одного-единственного DOM события. Для такого случая предусмотрен еще один вариант синтаксиса DOM событий, выглядящий, как DOM_EVENT::KEY(SELECTOR)
. KEY
, в данном случае — некий ключ, связанный с неким DOM элементом. а SELECTOR
— это селектор DOM элемента, который входит в элемент, связанный с KEY
.
<div class="my-node">
<span class="my-inner-node"></span>
</div>
this.bindNode('x', '.my-node');
this.on('click::x(.my-inner-node)', handler);
Если нам нужно создать обработчик для некоегого элемента, входящего в песочницу, используется немного упрощенный синтаксис DOM_EVENT::(SELECTOR)
.
Напомню, песочница ограничивает влияние экземпляра Матрешки или произвольного объекта одним элементом в веб приложении. Например, если на странице есть несколько виджетов, и каждый виджет управляется своим классом, очень желательно задать песочницу для каждого класса, указывающую на корневой элемент виджета, на который влияет этот класс.
this.bindNode('sandbox', '.my-node');
this.on('click::(.my-inner-node)', handler);
Этот код делает совершенно то же самое:
this.on('click::sandbox(.my-inner-node)', handler);
Напомню, MK.Object
— это класс, отвечающий за данные, типа ключ-значение. Кроме этого, он может использоваться, как модель в классе MK.Array
(собственно, как и сам MK.Array
, позволяющий создавать коллекции коллекций произвольной вложенности). Подробнее об этом классе прочтите в документации.
При каждом измении свойств, отвечающих за данные, генеируется событие "modify"
.
this.on('modify', handler);
Таким нехитрым способом можно слушать все изменения данных, вместо ручной прослушки свойств.
С массивом всё намного интереснее. MK.Array включает массу полезных событий, дающих возможность узнать что произошло в коллекции: вставка элемента, удаление элемента, пересортировка, какой метод был вызван…
Напомню, MK.Array
— это класс, отвечающий за реализацию коллекций во фреймворке Матрешка. Класс полностью повторяет методы встроенного Array.prototype
, а программисту не нужно думать о том, какой метод вызвать, чтоб что-то добавить или удалить. Что нужно знать о событиях MK.Array
:
- При вызове методов, позаимствованных у
Array.prototype
вызывается соответствующее событие ("push"
,"splice"
,"pop"
...) - При вставке элементов в массив генерируются события
"add"
и"addone"
. Используя первое, в свойство"added"
попадает массив из вставленных элементов. Используя второе в свойство"added"
попадает вставленный элемент, а событие генерируется столько раз, сколько элементов добавленно. - При удалении элементов используется та же логика:
"remove"
генерируется, передавая в свойство"removed"
объекта события массив удаленных элемеентов, а"removeone"
генерируется на каждом удаленном элементе, передавая в свойство"removed"
удаленный элемент. - При любых модификациях коллекции генерируется событие
"modify"
. Т. е. отлавливать события"remove"
и"add"
по отдельности не обязательно.
Несколько примеров из документации:
this.on('push', function(evt) {
console.log(evt.args); // [1,2,3]
});
this.on('add', function(evt) {
console.log(evt.added); // [1,2,3]
});
// обработчик запустится трижды,
// так как в массив добавили три новых элемента
this.on('addone', function(evt) {
console.log(evt.added); // 1 ... 2 ... 3
});
this.push(1, 2, 3);
Чтоб не копировать содержимое документации полностью, предлагаю ознакомиться с документацией к MK.Array самостоятельно.
Это, пока что, всё. Надеюсь, материал статьи оказался для вас простым и прозрачным. Матрешка ломает привычные стереотипы JavaScript, позволяя разработчику контролировать то, что он раньше не имел возможности контролировать. Проект развивается, в том числе, благодаря здравому скептицизму, который из статьи в статью появляется в комментариях, содержащих, кроме критики, очень интересные идеи (привет, lega, который задавал вопрос о том, что сейчас наывается «глубоким связыванием» и Delphinum, который использует Матрешку в связке с React, но не хочет использовать функционал классов). Еще раз напоминаю о том, что об изменениях к версии 1.1 можно найти в посте Matreshka.js 1.1: еще больше крутостей.
Спасибо всем, кто остаётся с проектом. Всем добра.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.