Matreshka.js: события

image
Документация на русском
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');


Это событие может быть полезно, например, тогда, когда байндинги контролирует другой класс, и вам нужно запустить свой код после какой-нибудь привязки (например, привязки песочницы).
image

Именно. Когда добавляется событие, генерируются события "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


image

У нас есть коллекция, элементы которой содержат свойство "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: еще больше крутостей.

Спасибо всем, кто остаётся с проектом. Всем добра.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

© Habrahabr.ru