(Архив) Matreshka.js v0.2

  • Tutorial
Статья устарела. См. актуальную историю версий.


Всем привет. Представляю очередное обновление фреймворка Matreshka.js до версии 0.2. Напомню: Матрешка — фреймворк общего назначения с окрытым исходным кодом, в идеологию которого положено доминирование данных над внешним видом: вы задаёте правила, как интерфейс должен синхронизированться с данными, затем работаете исключительно с данными, кроме случаев, когда событие интерфейса не касается данных (например, щелчек по кнопке или сабмит формы, сами по себе, не меняют данные, а запускают функцию, которая, в свою очередь, работает с данными)



Пример
Матрешка позволяет довольно просто связать данные и элементы представления (например, свойство объекта и значение поля ввода), не заботясь о дальнейшей синхронизации данных и представления. Например, самая простая привязка выглядит так:
<select class="my-select">
	<option>1</option>
	<option>2</option>
	<option>3</option>
</select>

Создаем экземпляр:
var mk = new Matreshka();

Связываем свойство x с элементом .my-select:
mk.bindElement( 'x', '.my-select' );

Меняем данные
mk.x = 2;

После того, как мы присвоим свойству x другое значение, остояние элемента изменися соответствующим образом.
Взгляните на живой пример

Другой важной чертой матрешки являются события (в том числе и кастомные). Например, Матрешка умеет отлавливать изменение значения свойства:
mk.on( 'change:x', function( evt ) {
	alert( 'x изменен на ' + evt.value );
});

Код выведет "x изменен на Привет":
mk.x = 'Привет';

Подробнее об этих и других фичах смотрите по ссылкам выше.

Ссылка на сайт Матрешки. Ссылка на github репозиторий.

Поддержка AMD


Матрешка теперь моддерживает спецификацию определения асинхронных модулей, Asynchronous Module Definition. Другими словами, Матрешка совместима с библиотеками, типа requirejs. Это значит, что теперь можно писать тру-код, не гадящий в глобальное пространство имен. Поддерживается два типа подключения: запрос именованного модуля и запрос безымянного модуля.

Именованные модули:
requirejs.config({
	paths: {
		xclass: 'path/to/matreshka',
		matreshka: 'path/to/matreshka'
	}
});
require(['xclass', 'matreshka'], function(Class, MK) {
	return Class({
		'extends': MK
		//...
	});
});

Но это, скорее, побочный эффыект использования новой файловой структуры проекта. А рекомендованный способ — запрос безымянного модуля:
require(['path/to/matreshka'], function( MK ) {
	return MK.Class({
		'extends': MK
		// ...
	});
});

Как видете, Матрешка содержит свойство Class, которое дублирует функцию, создающую классы: нет нужды запрашивать дополнительный модуль.

Метод Matreshka#addDependency: новое имя и дополнительные фичи


1. Метод addDependence был переименован в addDependency по подсказке хабраюзера buriy (спасибо ему), старый метод помечен, как «устаревший».
2. Метод теперь поддерживает обещанную возможность добавления зависимости от свойств других классов. Синтаксис второго аргумента таков: [ инстанс, "ключ", инстанс, "ключ", инстанс, "ключ" ... ] — массив, с нечетными элементами — экземплярами классов, четными — ключами этих экземпляров, от которых и зависит искомое свойство. Взгляните на пример:
this.addDependency( 'a', [
	anotherInstance1, 'b',
	this, 'c',
	anotherInstance2, 'd'
], function( b, c, d ) {
	return b + c + d;
});

Здесь свойство "a" зависит от свойства "b" объекта anotherInstance1, от свойства "d" объекта anotherInstance2 и от собственного свойства "c". Старый синтаксис по-прежнему работает:
this.addDependency( 'a', 'b c', function( b, c ) {
	return b + c;
});

3. Безопасные зависимости. Этот пункт никак не отражается на синтаксисе: начиная с этого релиза метод избегает бесконечного цикла при неправильном использовании addDependency. Представьте себе ситуацию, когда свойство "a" зависит от свойства "b", свойство "b" зависит от свойства "c", а свойство "c", в свою очередь, зависит от "a". Абстрактная иллюстрация к примеру:
this.addDependency( 'a', 'b', function( b ) {
	return b * 2;
});

this.addDependency( 'b', 'c', function( c ) {
	return c * 3;
});

this.addDependency( 'c', 'a', function( a ) {
	return a / 5;
});

Каждая зависимость в этом коде вызывала следующую, результатом чего получаем повисшую страницу. Теперь же появилась защита от таких ошибок: код передаёт через всю цепочку зависимостей специальный флаг, и, когда фреймворк доходит до потенциально опасной зависимости, цепочка останавливается. addDependency в новом виде позволяет строить взаимные зависимости на основе сложных (или не очень) формул, не опасаясь ошибок в реализации этих формул. Пример вычисления периметра прямоугольника по длинам сторон, и вычисления длин сторон:
this.addDependency( 'p', 'a b', function( a, b ){
	return (a + b) * 2;
});

this.addDependency( 'a', 'p b', function( p, b ){
	return p/2 - b;
});

this.addDependency( 'b', 'p a', function( p, a ){
	return p/2 - a;
});


Статичный метод Matreshka.procrastinate


Представьте себе следующую ситуацию (взята из моей практики). У вас есть форма с некими текстовыми полями: чекбосами и пр. Когда меняется значение одного из элементов формы, приложение должно отправить запрос на сервер, который, в свою очередь, возвращает данные для рендеринга трёх графиков. Рисование графиков — дело тяжелое для процессора и на слабом компьютере занимает полсекунды (Highcharts он такой). Теперь представьте пользователя, которому скучно и он решил многократно кликнуть на чекбокс. Что произойдет? Отправится куча запросов, вернется куча ответов, которые так же многократно отрисуют график. Что обычно делают в таком случае? Отменяют запрос на сервер. Спрашивается: зачем было этот запрос посылать, если можно было дождаться, пока тот угомонится? :)

Для решения этой задачи я использовал простейшую функцию (возможно, велосипед), которая принимает другую функцию в качестве аргумента и возвращает её модификацию, которая может быть запущена только однажды за определенный промежуток времени. Без нее не обходится ни один проект, поэтому было решено включить её в код Матрешки. Пример:
var doSomethingHeavy = function( i ) {
	console.log( 'Ok', i );
};

var procrastinateSomethingHeavy = MK.procrastinate( doSomethingHeavy );

for( var i = 0; i < 100; i++ ) {
	procrastinateSomethingHeavy( i );
}

// >> Ok 100

Код функции (на случай, если вы захотите использовать её вне Матрешки):
var procrastinate = function ( f, d, thisArg ) {
	var timeout;
	if( typeof d !== 'number' ) {
		thisArg = d;
		d = 0;
	}
	return function() {
		var args = arguments,
			_this = this;
		clearTimeout( timeout );
		timeout = setTimeout( function() {
			f.apply( thisArg || _this, args );
		}, d || 0 );
	};
};

Метод, кроме «прокрастинирующей» функции, принимает задержку, и контекст в качестве аргументов. Задержка отвечает за то, на сколько миллисикунд будет отложен реальный вызов функции при очередной попытке её вызова.

А вот пример случая, когда функция никогда не будет вызвана (для лучшего понимания).
var procrastinateSomethingHeavy = MK.procrastinate( function() {
	console.log( 'Ok' );
}, 1000 );

setInterval( function() {
	procrastinateSomethingHeavy();
}, 500 ); // интервал меньше задержки


Новый ключ привязчика initialize


Привязчик (binder) — третий аргумент метода Matreshka#bindElement. Если вы помните, это объект, состоящий из трех свойств: on (по какому DOM событию обновить свойство), getValue (как извлечь значение свойства из элемента), setValue (как установить значение свойства элементу). Подробнее вот здесь (кстати, все статьи о Матрешке обновляются каждый релиз и являются актуальным материалом). Теперь появился еще одно опциональное свойство initialize.

initialize — функция, запускающаяся во время привязки, а точнее, до неё. Задача функци — подсластить код. Взгляните на пример из первой статьи:
Во-первых, перед привязкой объявим слайдер:
<div class="slider"></div>

$( ".slider" ).slider({ min: 0, max: 100 });

Во-вторых объявляем экземпляр Матрешки:
var mk = new Matreshka();

Дальше вызываем привязку:
mk.bindElement( 'x', '.slider', {
  on: 'slide', // событие, по которому из элемента извлекается значение
  getValue: function() {
    return $( this ).slider( 'option', 'value' ); // как вытащить значение из элемента (см. документацию jQuery ui.slider)? 
  },
  setValue: function( v ) {
    $( this ).slider( 'option', 'value', v ); // как установить значение для элемента (см. документацию jQuery ui.slider)? 
  }
});

Код несколько избыточен: мы дважды обращаемся к элементу с классом slider (сначала, применяя плагин, затем привязывая элемент). Теперь этого можно избежать:
var mk = new Matreshka();

mk.bindElement( 'x', '.slider', {
  initialize: function() {
    $( this ).slider({ min: 0, max: 100 });
  },
  on: 'slide',
  getValue: function() {
    return $( this ).slider( 'option', 'value' );
  },
  setValue: function( v ) {
    $( this ).slider( 'option', 'value', v );
  }
});


Метод Matreshka#defineSetter


Этот новый метод, как не трудно догадаться, определяет сеттер для свойства.
this.defineSetter( 'x', function( value ) {
	return alert( value );
});

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

this.x = 1;

this.on( 'change:x', function( evt ) { // обраьотчик, который не сработает из-за перетертого сеттера
	alert( 'x is changed to ' + evt.value );
});

this.defineSetter( 'x', function() {
	// ...
});

this.x = 2;


Новый синтаксис для имен событий: добавление обработчикоов событий для свойств и элементов коллекции


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

Событие "ключ@имя_события"

Теперь можно добавить обработчик для свойства внутри любого класса, унаследованного от Матрешки (в том числе, для MK.Object и MK.Array), при условии, если значением свойства является экземпляр Матрешки. Взгляните на пример:
var mk = new MK;
mk.on( 'x@yeah', function() {
	alert( 'yeah' );
});

mk.x = new MK;
mk.x.trigger( 'yeah' );

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

Событие "@имя_события" для MK.Object

Такое имя события позволяет добавить обработчик для JSON ключа экземпляра MK.Object (что такое JSON ключ, или ключ, отвечающий за данные, смотрите в статье об MK.Object).
var mkObject = new MK.Object;

mkObject.on( '@yeah', function() {
	alert( 'yeah' );
});

mkObject.jset( 'x', new MK );

mkObject.x.trigger( 'yeah' );

Порядок объявления свойства и обработчика событий так же не важен.

Событие "@имя_события" для MK.Array

По аналогии с MK.Object, такую же возможность имеет и MK.Array: обработчик навешивается на любой из элементов массива, при условии, что этот элемент унаследован от Матрешки.
var mkArray = new MK.Array;

mkArray.on( '@yeah', function() {
	alert( 'yeah' );
});

mkArray.push( new MK );

mkArray[ 0 ].trigger( 'yeah' );

Эти три изменения не ограничтваются только лишь прослушкой события "yeah", можно с уверенностью слушать и другие события, например, "change:свойство"
this.on( 'x@change:y', function() { /* ... */ } );
this.on( '@change:y', function() { /* ... */ } );

Теоретически, эта фича позволяет строить причудливые имена событий, слушая другие события в глубине дерева данных. Скажем, у нас есть структура данных, которую можно изобразить в виде объекта:
{
	a: [{
		b: { c: { e: 1 } }
	}, {
		b: { d: { e: 2 } }
	}]
}

Для того, чтоб докапаться до изменений свойства "e", можно добавить такой обработчик:
this.on( 'a@@b@@change:e', function() { /* ... */ } );


Метод Matreshka#$bound


У Матрешки есть два метода, возвращающие привязанные элементы: Matreshka#bound, который возвращет первый привязанный элемент или null и Matreshka#boundAll, который возвращает коллекцию привязанных элементов. Здесь могут возникнуть проблемы у новичков, работающих с jQuery и не знакомых с VanillaJS в понимании термина «коллекция» и привыкших к знау доллара. Поэтому, во фреймворк был добавлен метод $bound делающий совершенно то же самое, что и Matreshka#boundAll.

this.bindElement( 'a', '#x, #y' );
this.$bound( 'a' ).animate( /* ... */ ); // применяем любой jQuery метод


Другие изменения


Matreshka.useAs$ вместо usejQuery и useBalalaika

Напомню, начиная с версии 0.1, Матрешка избавилась от жесткой зависимости jQuery, используя микро-библиотеку «Балалайка», если jQuery нет на странице. Седствием этогого изменения было создание двух методов, которые, не зависимо от наличия jQuery, заставляли Матрешку использовать одну з двух библиотек с помощью методов usejQuery (на случай, если jQuery была подключена после Матрешки) и useBalalaika (на случай, если jQuery был подключен раньше Матрешки, но вы всё равно хотите использовать встроенную библиотеку). Теперь плявился метод, который позволяет использовать вообще любую jQuery-подобную библиотеку (usejQuery и useBalalaika помечены, как устаревшие).

Примеры использования:
MK.useAs$( jQuery );
MK.useAs$( jQuery.noConflict() ); 
MK.useAs$( Zepto );
MK.useAs$( MK.$b ); // Балалайка

Следствием этого изменения стало то, что Матрешка, загружаясь, использует библиотеку знак-доллара, если такая есть и имеет определенные методы, вместо использования только лишь jQuery. Какие именно методы, можете узнать в исходном коде одного из файлов проекта.

Метод xclass.same

Небольшое изменение, добавляющее ситаксический сахар в классы (см. статью о наседовании). Часто, создавая класс, конструктору этого класса требуется, всего лишь, вызвать конструктор родителя в собственном контексте:
var MyClass = Class({
	'extends': AnotherClass,
	constructor: function() {
		AnotherClass.call( this, arguments );
	},
	someNewMethod: function() { /* ... */ }
});

Теперь то же самое можно сделать более кратко:
var MyClass = Class({
	'extends': AnotherClass,
	constructor: AnotherClass.same(),
	someNewMethod: function() { /* ... */ }
});


Добавление обработчиков DOM событий (например, "click::x") до того, как элемент был привязан

У Матрешки есть возможность навешивать обработчики событий на приязанные элементы с помощью метода Matreshka#on:
this.bindElement( 'x', '.my-element' );
this.on( 'click::x', function() {
	alert( '.my-element is clicked' );
});

Проблема в том, что нельзя было добавить обработчик DOM собтия до того, как элемент был привязан. Приходилось извращаться ожиданием события bind и добавлением обработчика по наступлению этого события:
this.on( 'bind:x', function() {
	this.on( 'click::x', function() {
		alert( '.my-element is clicked' );
	});
});

this.bindElement( 'x', '.my-element' );

Теперь порядок привязки/добавления DOM события не важен:
this.on( 'click::x', function() {
	alert( '.my-element is clicked' );
});
this.bindElement( 'x', '.my-element' );


Исправленные ошибки/рефакторинг


  • Matreshka.Array#initializeSmartArray (документация к методу в работе) теперь возвращает this
  • Matreshka.Array#createFrom принимает undefined в качестве аргумента
  • Изменены случаи, когда срабатывает событие "modify" для класса Matreshka.Array
  • Матрешка теперь вызывает событие "delete" при удалении свойства вместо "remove" потому что у Matreshka.Array есть событие с таким же именем, но вызываемое в другом случае
  • Если [].forEach не существует, генерируется ошибка с предложением подсключить es5-shim
  • Исправлен баг в парсере Балалайки
  • Исправлен баг в методе Matreshka#once, теперь обработчик может быть удален с помощью метода Matreshka#off
  • Теперь триада eventName + eventHandler + context может быть добавленна только раз на один экземпляр
  • Исправил баг в функции Class (splice vs slice)
  • Рефакторинг методов Matreshka#on and Matreshka#off
  • Небольшой рефакторинг Matreshka#trigger и MK#set


Что дальше?


1. В следующей статье я ознакомлю вас с реализацией TodoMVC. Статья, уже готова, но требует редактирования. Реализация тоже готова, но для нее допиливается документация.
2. После этого планируется большая статья о MK.Array, заменяющая предыдущую. Там я расскажу подробне о методах, о том, как рендерятся элементы массива, о «модели» и о том, как передавать опции в методы массивов.
3. Версия 0.3 с кучей интересных изменений, которые уже тестируется. Как обычно, будет статья.

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

Всем добра!
Matreshka.js 17,72
JavaScript фреймворк для новичков
Поделиться публикацией
Похожие публикации
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама
Комментарии 10
  • 0
    Просто оставлю это здесь: Микропаттерны оптимизации в Javascript: декораторы функций debouncing и throttling

    В целом интересно, но вот не покидает ощущение, что слишком сложно, как API, так и использование. Сделали бы пример, как это упростило вам жизнь (было → стало).
    • 0
      Спасибо за статью, я подозревал, что идея procrastinate не оригинальна, о чем упомянул в посте.
      Лучше всего «было → стало» объяснит реализация TodoMVC. Так можно будет сравнить Матрешку с десяткамит других фреймворков (для тех, кто нге дочитал статью до конца, реализация будет представлена скоро).
      • +1
        В том то и дело, TodoMVC непоказательно, оно больше подходит для MV* решений (да и то очень спорно), чем не является матрешка, которая больше похожа на сборник «полезных инструментов», а не базы для создания SPA. В статье вы упоминаете работу с формами, чем не отличный пример, чтобы разобрать.
        • 0
          Пример работы с формой есть в статье «Matreshka.js — Наследование».

          Я в курсе о проблеме изложения, сейчас думаю, как лучше структурировать обучающий материал: в виде статей, видеоуроков, книги и пр. для того, чтоб всем было просто разобраться в том, как строить неограниченные по масштабу SPA.
    • +1
      Спасибо, про Object.defineProperty не знал.

      Сложновато для понимания, но идея интересная. К сожалению все примеры которые есть на сайте демонстрируют больше сложность, чем применимость. Вот взять к примеру Backbone, там на основе модели рисуется вид. У вас на основе переменных делается тоже самое, но Backbone легче применить.
      • –1
        Проблема Backbone — излишние сущности. Нужно постоянно помнить о том, что, данные и вьюха связаны событиями в разных файлах. В Матрешке вы однажды задаёте правило, как свойство реагирует на привязанный элемент, и как элемент реагирует на изменения свойства (причем «монолитно»: в одном месте, одной функцией) и работаете исключительно с данными. Другими словами, Матрешка — замена Бекбона для ленивых :)
        • 0
          Что-то вы не то говорите, в Backbone как раз всё на своих местах. Модель/Коллекция и View (вот родной View можно заменить на что-то получше c data-binding), можно даже подружить с React. Помимо этого, описание взаимодействия точно также находится в одном «файле», т.е. модуле… ну так далее.
          • 0
            Уверен, вы знаете, о чем говорите. Но из-за того, что не существует единственного правильного пути разработки приложения, у нас есть большой выбор среди фреймворков. Матрешка — фреймворк, созданный под влиянием Backbone (в первой статье даже есть упоминание о том, что часть кода, отвечающая за события позаимствован от туда) и решающий ряд проблем, с которыми, вы, видимо, еще не столкнулись: большое приложение требует неоправданно много человеко-мозго-часов. Взгляните на реализацию TodoMVC на Backbone: слишком много избыточного кода, и, как следствие, файлов. Несмотря на то, что вы не считаете TodoMVC показательным примером, я всё же настою, что каждая реализация, как раз-таки — лицо соответствующего фреймворка. Другое дело — Ангуляр. Несмотря на спорный подход (логика в HTML коде), этот фреймворк считаю, в целом, хорошим и интересным.
            • 0
              Я специально обратил внимание на то, что родной View у Backbone нужно заменить и как только мы это делаем, всё становиться на свои месте. Все эти человеко-часы точно также будут и у любого человека, с любым FW, если он его не знает. Ангуляр тоже на первых парах прост и удобен, но чем дальше в лес, тем больше проблемы, о чем говорят многочисленные статьи. Тоже самое будет и с вашим инструментом, когда его будет использовать другой человек, а может и хуже, кто знает.
              • 0
                Вы правы, что опасаетесь новых, неопробованных инструментов. На сегодняшний день, вам остаётся мне верить или не верить наслово. Туториалы по Матрешке выходят редко, я стараюсь это исправить. Пока что, могу сказать одно: для того, чтоб написать неограниченное в размерах приложение, нужно соблюдать простое правило: один виджет — один класс. Это довольно старая (со времен появления ООП) и успешная практика. Скажу честно, о проблемах Ангуляра слышу впервые, возможно, из-за того, что не строил на нем чего-то большого.

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

      Самое читаемое