Matreshka.js v0.2

в 6:38, , рубрики: javascript, jquery, Matreshka, Веб-разработка

Всем привет. Представляю очередное обновление фреймворка 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 с кучей интересных изменений, которые уже тестируется. Как обычно, будет статья.

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

Всем добра!

Автор: Finom

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js