Matreshka.js v0.2
Всем привет. Представляю очередное обновление фреймворка Matreshka.js до версии 0.2. Напомню: Матрешка — фреймворк общего назначения с окрытым исходным кодом, в идеологию которого положено доминирование данных над внешним видом: вы задаёте правила, как интерфейс должен синхронизированться с данными, затем работаете исключительно с данными, кроме случаев, когда событие интерфейса не касается данных (например, щелчек по кнопке или сабмит формы, сами по себе, не меняют данные, а запускают функцию, которая, в свою очередь, работает с данными).Пример Матрешка позволяет довольно просто связать данные и элементы представления (например, свойство объекта и значение поля ввода), не заботясь о дальнейшей синхронизации данных и представления. Например, самая простая привязка выглядит так: Создаем экземпляр: 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 — функция, запускающаяся во время привязки, а точнее, до неё. Задача функци — подсластить код. Взгляните на пример из первой статьи:
Во-первых, перед привязкой объявим слайдер:
$(».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 с кучей интересных изменений, которые уже тестируется. Как обычно, будет статья.Затем, грядет большое ревью документации, в том числе, с причесыванием текстов и исправлению ошибок, касающихся английского языка. Текст главной страницы и страницы «Почему Матрешка?» уже исправлен.
Всем добра!