- Введение
- Наследование
- MK.Object
- MK.Array
- Matreshka.js v0.1
- Matreshka.js v0.2
Всем привет. Представляю очередное обновление фреймворка 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
vsslice
) - Рефакторинг методов Matreshka#on and Matreshka#off
- Небольшой рефакторинг Matreshka#trigger и MK#set
Что дальше?
1. В следующей статье я ознакомлю вас с реализацией TodoMVC. Статья, уже готова, но требует редактирования. Реализация тоже готова, но для нее допиливается документация.
2. После этого планируется большая статья о MK.Array
, заменяющая предыдущую. Там я расскажу подробне о методах, о том, как рендерятся элементы массива, о «модели» и о том, как передавать опции в методы массивов.
3. Версия 0.3 с кучей интересных изменений, которые уже тестируется. Как обычно, будет статья.
Затем, грядет большое ревью документации, в том числе, с причесыванием текстов и исправлению ошибок, касающихся английского языка. Текст главной страницы и страницы «Почему Матрешка?» уже исправлен.
Всем добра!
Автор: Finom