Matreshka.js 1.1: еще больше крутостей

image
Документация на русском
Github репозиторий

Всем привет. Сегодня, 28 сентября исполняется два года с первого коммита в репозиторий Матрешки. Так совпало, что к этому времени подоспел новый релиз со всякими вкусностями для любого JavaScript разработчика (даже для тех, кто не хочет использовать Матрешку в качестве фреймворка).

Матрешка — это JavaScript фреймворк (или, если хотите, библиотека), основанный на акцессорах, и выжавшая из них невероятные, на первый взгляд, возможности. Помните, время, когда в JavaScript геттеры и сеттеры только-только появились? Сколько шума было вокруг них… Статьи, разговоры… Затем, всё затихло: многие не понимали, как этими возможностями воспользоваться, кроме как в простых примерах. Матрешка — это прекрасный ответ на вопрос, зачем нужны акцессоры в JavaScript.

По традиции, напомню о том, что умеет этот фреймворк с помощью маленького кусочка кода.

Раньше можно было делать только так:

// this - экземпляр Матрешки
// связываем свойство "x" с элементом на стрнице
this.bindNode('x', 'input.my-node');
// если изменилось, выводим alert
this.on('change:x', function() {
    alert(this.x);
});
// меняем свойство, вызывается обработчик
// меняется и привязаннык к "x" элемент
this.x = 'Wow!';


Теперь можно еще и так:

var object = {};
// связываем свойство "x" с элементом на стрнице
MK.bindNode(object, 'x', 'input.my-node');
// если изменилось, выводим alert
MK.on(object, 'change:x', function() {
    alert(object.x);
});
// меняем свойство, вызывается обработчик
// меняется и привязаннык к "x" элемент
object.x = 'Wow!';


Из-за того, что последние версии Chrome и NodeJS стали, наконец, поддерживать большинство элементов синтаксиса ES6, все примеры ниже в этом посте будут написаны на ES6. Таким нехитрым способом я хочу поздравить всех, кто считает эти нововведения невероятно крутыми и привлечь внимание к ES.next тех, кто с ними еще не знаком.
Самым главным нововведением стала поддержка Матрешкой произвольных объектов. Да-да, для того, чтоб объявить байндинг или сделать еще что-нибудь крутое, не обязательно создавать экземпляр Матрешки.

let object = {};
//связываем свойство "x” с элементом  '.my-node'
MK.bindNode(object, 'x', '.my-node');
// "y” всегда будет суммой значений свойств x и z
MK.linkProps(object, 'y', 'x z', (x, z) => x + z);
//”z” - это всегда число, независимо то того, какой тип мы ему присвоим
MK.mediate(object, 'z', Number);
// ...


Как видно из примера, новые статичные методы полностью повторяют динамические с одним маленьким отличием: им нужно передать исходный объект в качестве первого аргумента:

//было
this.bindNode('x', '.my-node');
//стало
MK.bindNode(object, 'x', '.my-node');


Кроме этого, коллекции, умеющие сами себя рендерить, теперь не требуют указания модели, которая раньше должна была наследоваться от класса Matreshka.

class MyArray extends MK.Array {
        itemRenderer() {
                return '<li>';
        }

        constructor() {
                super().bindNode('sandbox', '.some-node');
        }
}

let arr = new MyArr();
arr.push(someData);


Полный список новых статичных методов перечислен в разделе MatreshkaMagic.
Благодаря поддержке нативных объектов появиласть возможность вынести весь «магический» функционал в отдельную, более компактную библиотеку, не включающую в себя классы Matreshka, Matreshka.Array, Matreshka.Object и функцию Class. Разработчику доступен объект MatreshkaMagic или более краткий вариант magic, содержащий все статичные методы класса Matreshka.

Библиотека находится в папке magic/ репозитория.

magic.bindNode(object, 'x', '.my-node');
magic.linkProps(object, 'y', 'x z', (x, z) => x + z);
// и т. д.


Подробнее о библиотеке в документации.
Еще одна крутая фича, появившаяся благодаря поддержке нативных объектов — это так называемое «глубокое связывание». Имея объект произвольной вложенности, можно связать DOM ноду с любым свойством где-то в глубине этого объекта.

this.a = {b: {c: {d: 41}}}
this.bindNode('a.b.c.d', '.my-node');


Ядро Матрешки следит за всей веткой объектов и переустанавливает байндинг если один из объектов ветки переопределен

this.a.b = {c: {d: 42}};


Матрешка давно включает в себя метод linkProps, позволяющий задать зависимость одних свойств от других.

Можно задать зависимость от собственных свойств:

this.linkProps('a', 'b c d', (b, c, d) => b + c + d);
this.b = 1;
this.c = 2;
this.d = 3;
alert(this.a); // 6


Можно задать зависимость от свойств других объектов:

this.linkProps('a', [
        externalObject, 'b',
        externalObject2, 'c',
        this, 'd'
]);

externalObject.b = 1;
externalObject2.c = 2;
this.d = 3;
alert(this.a); // 6


Теперь linkProps поддерживает указание пути к свойству:

this.linkProps('a', 'b.c.d e.f', (d, f) => d + f);
this.b = {c: {d: 40}};
this.e = {f: 2};
alert(this.a); // 42


Когда в цепочке пути к свойству что-то меняется, Матрешка перехватывает это изменение, разрывает связь со старой подцепочкой и создаёт зависимость от новой цепочки.

this.b.c = {d: 1};


По-прежнему, можно создавать зависимость от свойств других объектов, при этом, как упоминалось выше, в качестве исходного объекта могут выступать любой объект:

let targetObject = {},
        o1 = {b: {c: {d: 40}}},
        o2 = {e: {f: 2}};

MK.linkProps(targetObject, 'a', [
        o1, 'b.c.d',
        o2, 'e.f'
], (d, f) => d + f);

alert(targetObject.a); // 42


Напомню, что в предыдущих версиях можно было навешать событие не только на текущий объект (this), но и на объект произвольной вложенности. Но синтаксис заставлял желать лучшего. Приведу небольшой пример. Скажем, у экземпляра Матрешки есть или должно появиться некое свойство, которое, в свою очередь, так же является экземпляром Матрешки.

this.a = new Matreshka();


Вы могли до или после присваивания свойства создать обработчик любого события, которое относится к этому свойству. Для этого использовался синтаксис с «собачкой».

this.on('a@someevent', handler);
this.a.trigger('someevent');


Для массивов и объектов (которые в Матрешке являются коллекцями типа ключ-значение), можно было не указывать целевое свойство, так как событие слушается во всех элементах, входящих в коллекцию:

this.on('@someevent', handler);
this.push(new Matreshka());
this[0].trigger('someevent');


На первый взгляд, выглядит просто. Но что если наше дерево объектов немного сложнее? Например, свойство "a" содержит коллекцию:

this.a = new MK.Array();
this.a.push(new Matreshka());


Как отловить событие, внутри такой коллекции? Можно скомбинировать две собачки, которые говорят: «в объекте "a" словить событие "@someevent" -> в элементе массива словить событие "someevent"».

this.on('a@@someevent', handler);
this.a[0].trigger('someevent');


Это еще можно пережить (если выпить достаточное количество кофе). А что, если мы хотим пойти глубже? Тогда количество «собачек” увеличится и кофе уже не поможет…

Согласитесь, потенциал у этой фичи очень велик. Мы можем слушать события данных любой вложенности, например, узнать об изменении свойства объекта, содержащегося в массиве массивов и пр. Поэтому, было решено несколько изменить синтаксис делегированных событий. „Собачка” осталась, но в качестве единственного разделителя пути к объекту и имени события. Если событие касается вложенного объекта, собачки заменяются на точки. Если мы хотим узнать о чем-то в коллекции, вместо безликой собачки используем звездочку. Тут мне, наверное, нужно остановиться и привести пару примеров.
Если мы хотим навешать обработчик на свойство "а" то синтаксис остается прежним:

this.on('a@someevent', handler);


Если мы хотим отловить событие у элемента коллекции, то вместо такого:

this.on('@someevent', handler);


Пишем так:

this.on('*@someevent', handler);


Звездочка значит “любое свойство, отвечающее за данные в MK.Object” или „любой элемент коллекции MK.Array”.

Идем глубже. Нам нужно причесать следующий пример, описанный выше:

this.on('a@@someevent', handler);


Теперь пишем так:

this.on('a.*@someevent', handler);


Синтаксис стал намного чище. Вам нужно просто указать путь к объекту перед @, а после неё указать имя события.

Подробная статья о событиях.


setClassFor — это еще одна невероятно крутая функция. Она указывает на то, экземпляром какого класса должно являться заданное свойство. При попытке перезаписать свойство, внутренний перехватчик, вместо присваивания, обновляет его новыми данными. Разберемся на примере.

// задаём свойству изначальные данные (не обязательно)
this.x = {a: 41};
// устанавливаем класс для свойства
this.setClassFor('x', MyClass);
// проверяем, является ли свойство экземпляром класса MyClass
console.log(this.x instanceof MyClass); // true
// проверяем свойство "a” экземпляра
console.log(this.x.a); // 41
// теперь самое интересное
// сохраняем значение "x” в одноименную переменную
var x = this.x;
// пытаемся перезаписать свойство
this.x = {a: 42};
// проверяем, обновились ли данные
console.log(x.a); // 42
// проверяем, перезаписаось ли свойство на самом деле
console.log(x === this.x); // true
// Wow! Экземпляр класса не изменился, а данные обновились!


Если у вас есть глубокая структура объектов и во вложенных объектах тоже запущен setClassFor, можно делать интересные вещи. Например, сохранять представление многоуровневых данных в локальном хранилище.

localStorage.x = JSON.stringify(this.x);


А потом восстанавливать их взмахом волшебной палочки:

this.x = JSON.parse(localStorage.x);


Либо, гонять туда-сюда на сервер.

Случаев, где такая логика может понадобиться невероятно много. В качестве еще одного примера, приведу код из документации (для краткости используются class properties из ECMAScript 7):

// app.js
class App extends MK {
        constructor(appData) {
                this.appData = appData;
                this.setClassFor('appData', AppData);
        }
}

// app-data.js
class AppData extends MK.Object {
        constructor(data) {
                super(data)
                        .setClassFor({
                                friends: Friends,
                                settins: Settings
                        });
        }
}

// friend.js
class Friend extends MK.Object {
        constructor(data) {
                super(data);
        }
}

// friends.js
class Friends extends MK.Array {
        Model = Friend;
        constructor(data) {
                super(...data);
        }
}

// settings.js
class Settings extends MK.Object {
        constructor(data) {
                super(data)
                        .setClassFor('credentials', Credentials);
        }
}

// credentials.js
class Credentials extends MK.Object {
        constructor(data) {
                super(data);
        }
}

// app-init.js
var app = new App({
        settings: {
                name: 'Vasiliy Vasiliev',
                credentials: {
                        email: 'vasia.vasia@gmail.com'
                }
        },
        friends: [{
                name: 'Yulia Zuyeva',
                id: 1
        }, {
                name: 'Konstantin Konstantinopolsky',
                id: 2
        }, {
                name: 'nagibator3000',
                id: 3
        }]
});

// данные можно сериализировать и передать на сервер
JSON.stringify(app.appData);


// потом просто присвоить новые данные свойству appData
// при этом, структура классов не изменится
app.appData = {
        settings: {
                name: 'Petr Petrov',
                credentials: {
                        email: 'petr.petrov@gmail.com'
                }
        },
        friends: [{
                name: 'Yulechka Zuyeva',
                id: 1
        }, {
                name: 'Konstantin Konstantinopolsky',
                id: 2
        }]
};


Более подробно в документации к методу.
Матрешка — это фреймворк, который исповедует идею о том, что логика должна содержаться в JS файлах, в противовес фреймворкам, реализующим паттерн MVVM, принуждающим описывать логику в HTML коде.

Имплементация логики в JS файлах — это действительно очень удобно. Но, порой, возникают ситуации, когда описание всех байндингов слишком затратно в плане количества строк кода.

Поэтому, было решено улучшить и ускорить DOM шаблонизатор, отсутствующий до недавнего времени в официальном API. Что он делает? Он берет DOM узел, коллекцию DOM узлов, HTML код или песочницу текущего объекта, разбирает его, находя ангуляр-подобные конструкции типа {{KEY}} и создает привязки там, где эти конструкции найдены.

<a href="http://{{website.domain}}/{{category}}/{{page}}">Look at the {{info.title}}</a>

this.parseBindings();
this.website.domain = 'example.com';
this.category = 'foo';
this.page = 42;
this.info.title = 'cool stuff';


Подробнее метод описан в документации.

Метод не противоречит идеологии Матрешки, так как логики (циклов, условий, обработчиков) в шаблоне быть не может.

Кроме публикации API самого метода, шаблонизатор для коллекций теперь включен по умолчанию (не нужно больше писать useBindingsParser: true).

class MyArray extends MK.Array {
        itemRenderer = '<span>Hello, {{name}}</span>';
        ...
}

Из примера к setClassFor видно, что методы запускаются сразу после super(). Эта возможность стала реальной благодаря очень простому изменению: все три конструктора (Matreshka, Matreshka.Array, Matreshka.Object) возвращают this вместо undefined.

class MyObject extends MK.Object {
        constructor(data) {
                super(data)
                        .bindNode('x', '.my-node');
        }
}

// создаст экземпляр MyObject со свойствами a и b,
// отвечающими за данные
myObject = new MyObject({a: 1, b: 2});

class MyCollection extends MK.Array {
        constructor(data) {
                super(...data)
                        .bindNode('x', '.my-node');
        }
}

// создаст коллекцию, состоящую из 5 элементов
myCollection = new MyCollection([1,2,3,4,5]);


В предыдущих версиях разработчику был доступен единственный синтаксис объявления обработчиков события.

this.on('eventname1', handler1);
this.on('eventname2', handler2);


Теперь же можно объявить несколько обработчиков, вызвав соответствующий метод лишь однажды:

this.on({
        'eventname1': handler1,
        'eventname2': handler2
});


Эта новость не упоминалась бы в этом посте, если бы не одно но: используя ECMAScript 2015 можно сильно укоротить код в микрозадачах.

this.on({
        'eventname1': evt => this.x = 42,
        'eventname2': evt => doSomethingElse()
});


Против “старого” синтаксиса:

this.on({
        'eventname1': function(evt) {
                this.x = 42
        },
        'eventname2':  function(evt) {
                doSomethingElse();
        }
});


itemRenderer — это виртуальное свойство коллекции (Matreshka.Array), которое говорит о том, как отрисовывать элементы коллекции.

// для краткости, синтаксис ES7
class MyCollection extends MK.Array {
        itemRenderer = "<div>Hi there!</div>";
        constructor() {
                super()
                        .bindNode('sandbox', '.array-sandbox')
                        .push({a: 1}, {a: 2});
        }
}


Более подробно в документации к itemRenderer.

Начиная с новой версии, при переопределении itemRenderer, коллекция автоматически перерисовывается.

//каждый элемент коллекции - span
this.itemRenderer = '<span>I'm a span</spam>';

//каждый элемент коллекции - div
this.itemRenderer = '<div>I'm a div</div>';


Можно придумать несколько юз-кейсов: вы хотите одной кнопкой изменить дизайн коллекции либо же ваш шаблон находится на сервере (в примере ниже используется Fetch API).

fetch('templates/my-template.html')
        .then(resp => resp.text())
        .then(text => this.itemRenderer = text);


Вы можете работать с коллекцией как обычно: вставлять, удалять, сортировать элементы, при этом, не нужно ждать пока сервер отдаст шаблон. По возвращении с сервера, коллекция сама отрисуется на странице.
Напомню, байндер — это объект, который указывать на то, как связать свойство с элементом на странице. Байндер используется методом bindNode, реализующим одно или двух-сторонее связывание.

this.bindNode('x', '.my-node', binder);
this.x = 42; // элемент на странице тоже изменился


Подробное опсисание можно найти в документации к методу.

MK.binders.progress — связывает свойство с состоянием HTML5 элемента progress. Байндер не нужно вызывать вручную, так как он входит в коллекцию стандартных байндеров.

this.bindNode('x', '.my-progress');
this.x = 42; // меняет значение прогресса на 42


MK.binders.innerText — связывает свойство с текстовым значением любого элемента, у которого есть свойство textContent или innerText.

this.bindNode('x', '.my-node', MK.binders.innerText());
this.x = 'Some <i>Text</i>'; // задаст ноде содержимое "как есть”, в виде текста


MK.binders.style — связывает свойство объекта со свойством объекта style элемента.

this.bindNode('x', '.my-node', MK.binders.style('color'));
this.x = 'red'; // изменит цвет текста на красный


И самое интересное: MK.binders.file. Этот новый байндер не только отловит изменение пользователем содрежимого input[type=”file”], но и прочтет файл в том формате, который вам нужен:

this.bindNode('x', '.my-file', MK.binders.file('dataURL'));

// событие изменения генерируется, когда файл прочитан
this.on('change:x', function() {
        console.log(this.x.readerResult); // "data:image/png;base64,iVBO..."
});


Подробнее в документации к байндеру.
Теперь, при использовании вышеперечисленных байндеров, ядро Матрешки проверит, есть ли в текущем объекте соответствующее свойство и, если ответ отрицательный, извлечет значение из элемента и присвоит его свойству.

<div class="my-div">Some data</div>

// скажем, что this.x не определен.
this.bindNode('x', '.my-div', MK.binders.innerHTML());
alert(this.x); //"Some data"


У Matreshka.Array появился новый виртуальный метод onItemRender. Он вызывается тогда, когда один из элементов коллекции был отрисован. Метод делает код более плоским, позволяя избежать прослушивания события "render".

Событие "render" всегда было стандартным паттерном, позволяющим добавить необходимые привязки при рендеринге.

class MyCollection extends MK.Array {
        Model: MyModel;
        itemRenderer = '<li>';
        constructor() {
        super()
                .bindNode('sandbox', '.array-sandbox')
                .on('*@render', evt => {
                        evt.self.bindNode(...); 
                });
        }
}


Теперь можно сделать так:

class MyCollection extends MK.Array {
        Model: MyModel;
        itemRenderer = '<li>';
        constructor() {
                super()
                        .bindNode('sandbox', '.array-sandbox');
        }

        onItemRender(item, evt) {
                item.bindNode(...);
        }
}


У „моделей” появился похожий виртуальный метод: onRender.
Раньше было так:

class MyModel extends MK.Object {
        constructor() {
                super()
                        .on('render', evt => {
                                this.bindNode(...);     
                        });
        }
}



Теперь можно писать так:

class MyModel extends MK.Object {
        constructor() {
                super()
        }

        onRender() {
                this.bindNode(...);
        }
}


После объявления связывания данных и DOM ноды, разработчик мог получить доступ к связанным узлам с помощью методов bound и $bound. bound возвращает первый привязанный элемент, $bound — все элементы в виде коллекции jQuery или Balalaika.

this.bindNode('x', '.my-node');
var boundNode = this.bound('x');
var allBoundNodes = this.$bound('x');


Свойства nodes и $nodes позволяют делать то же самое, но практически бесплатно, с точки зрения производительности, так как эти свойства являются обычными объектами.

this.bindNode('x', '.my-node');
var boundNode = this.nodes.x;
var allBoundNodes = this.$nodes.x;


Matreshka.to конвертирует произвольный объект в экземпляры MK.Object и MK.Array.
MK.Array.of, работающий так же, как и Array.of, но возвращающий экземпляр MK.Array.
MK.Array.from, работающий так же, как и Array.from, но возвращающий экземпляр MK.Array.
MK.trim для браузеров, которые не поддерживают String.prototype.trim.
MK.toArray, конвертирующий array-like массив в нативный Array в два раза быстрее, чем это делает Array.prototype.slice.
С помощью микрооптимизаций (например, использования цикла for..in вместо функции each) и более крупных изменений, получилось добиться отличных результатов. Например, в бенчмарке с небольшими коллекциями (10 элементов), Матрешка отставала от React на 10-20 процентов в Хроме и Файерфоксе (хотя и обгоняла в бенчмарках с большим коичеством элементов коллекции). Теперь в этом же тесте, Матрешка быстрее Реакта на 50 процентов в Хроме, и быстрее в 3 раза в Файерфоксе.

Вот список бенчмарков, чтобы убедиться самостоятельно: 10 элементов, 50 элементов, 100 элементов, 500 элементов, 1000 элементов.


Матрешка, наконец, тестируется автоматически. На момент написания этой статьи, реализовано 148 тестов, проверяющих работоспособность методов до того, как они попадут в бранч develop. Особенно скурпулезно оттестированы делегированные события, которые обязаны работать при самых разных разных обстоятельствах и ничего не ломать при этом.
На сайте документации появилось предупреждение о том, что использование Матрешки в Internet Explorer 8 не рекомендуемо по причине наличия массы методов, которые невозможно реализовать для этой версии “осла». На деле, это лишь дисклеймер на случаи, когда разработчик пытается бездумно использовать такие методы. Следует помнить лишь одно: статичные методы, добавляющие «магию» в нативные объекты не работают в ИЕ8.

Такой код сработает в IE8, в случае, если this — экземпляр Матрешки.

this.bindNode('key', '.node');


И такой сработает:

var mk = new Matreshka();
mk.bindNode('key', '.node');


А этот код сработает только в IE9+ и в других браузерах (в том числе и в древних WebKit и Opera Mini):

var object = {};
MK.bindNode(object, 'key', '.node');


Если очень руки чешутся использовать статичные методы в восьмом осле, можете предварительно сконвертировать объект в экземпляр Матрешки:

var object = MK.to({});
MK.bindNode(object, 'key', '.node');


Таким образом семантичное версионирование соблюдено.
— Синтаксис bindNode немного расширился.
— Ошибки об отсутствующих нодах, используя метод bindNode более информативны: теперь в тексте, кроме ключа, указывается селектор (если передан селектор).
— Исходный код разбит на мелкие составляющие.
— Из кода и из примеров на сайте убраны ненужные пробелы (f(x) вместо f( x )).
— Как уже писалось выше, Матрешка поддерживает Opera Mini и старые WebKit.

Об остальных изменениях и списке исправленных ошибок, можно узнать в соответствующем разделе на сайте.


Посмотреть на то, что сделано и планируется сделать, можно в Trello. Там же можно и проголовать, повышая приоритет карточек.

В чате Gitter довольно часто происходят обсуждения новых фич. Из-за этого, вопросы, которые задают пользователи, и ответы на них теряются где-то в темных подвалах интенета. Поэтому, было принято решение запустить, в качестве эксперимента, форум на базе Muut (на русском и на английском). Если возникает вопрос, не стесняйтесь его задавать там (даже если вы думаете, что вопрос глупый).

Для примера, вот один из отличных вопросов Rendol с равернутым ответом на него:

Приветствую!
Снова тот же вопрос, который у меня ранее уже был, но ответ я на него так и не нашел красивый.
Например переписка как в ВК:

User = {
  id: 5,
  name: 'Fedor',
  online: true
}


Этого пользователя мы размещаем в разных комнатах: room1, room2, room3.
Если User.online = false, то во всех 3х комнатах, должен измениться цвет например.
Т.о. 3 коллекции, которые содержат один объект и при этом этот объект выводится в 3х местах.
Примечание: не обязательно, что эти коллекции будут однотипными (не только комнаты), могут быть разного вида и представления.
Есть ли возможность привязать один объект к нескольким представлениям?


Ответ:

Да, коллекции, содержащие объект могут быть разными. Для этого у него должно быть установлено свойство bindRenderedAsSandbox: false, так как при вхождении в несколько коллекций, песочницей станет сразу несколько элементов (например, несколько разных строк из разных таблиц, а это сильно усложнит вам жизнь). Поэтому отключаем песочницу. При срабатывании события render можно определить, в какую именно коллекцию вставлена модель и, исходя из этого, объявить привязки.
Вот небольшой пример накатал: jsbin.com/cidise/12/edit. Объект user находится сразу в двух коллекциях (в таблице и списке ul), которые рендерятся по-разному. Можете написать в консоли tableUsers[0].name = 'xxx' и все узлы, привязанные к данному юзеру, изменятся. Получается, что не нужно создавать много отдельных объектов и синхронизировать их значения.
ЗЫ. При поддержке IE 8, для проверки того, является ли объект инстансом класса, нужно использовать метод .instanceOf

object.instanceOf( MyClass );
// вместо
object instanceof MyClass



Надеюсь, пост оказался полезным. Спасибо за внимание.

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

© Habrahabr.ru