По стопам Meteor или велосепедируем реактивность

от автора


Доброе время суток, хабраюзер! Сегодня мы попытаемся немного разобрать реактивность, которая лежит в основе одного из самых хипстерских фреймворков — Meteor.

Для начала приведем немного сухой теории из Википедии:
Реактивное программирование — парадигма программирования, ориентированная на потоки данных и распространение изменений. Это означает, что должна существовать возможность легко выражать статические и динамические потоки данных, а также то, что выполняемая модель должна автоматически распространять изменения сквозь поток данных.

К примеру, в императивном программировании присваивание a = b + c будет означать, что переменной a будет присвоен результат выполнения операции b + c, используя текущие (на момент вычисления) значения переменных. Позже значения переменных b и c могут быть изменены без какого-либо влияния на значение переменной a.

Сразу оговорюсь, что данный пост нацелен больше на садистов новичков, нежели на матерых покровителей JS.

* велосепедируем — пишем свой велосипед, подглядывая на уже готовую реализацию.

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

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

var Deps = {   funcs: [], // массив функций, в которых творится реактивность   vars: [] // массив реактивных переменных }; 

Каждый раз, когда мы будем создавать новое реактивное окружение, в оба массива будут вставляться нужные нам данные. В частности, в массив funcs у нас будет попадать функция, внутри которой располагаются реактивные переменные, за судьбой которых надо следить. В массив vars же под тем-же индексом будет попадать массив реактивных переменных, за судьбой которых и следит наше реактивное окружение.

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

var Tracker = function(fn) {   Tracker.active = true; // устанавливаем флаг   var count = Deps.funcs.push(fn); // вставляем функцию в список трекеров   Deps.vars[count - 1] = []; // инициализируем массив для помещения туда реактивных переменных    fn();    Tracker.active = false; }; 

На данный момент все это бесполезно потому что у нас нет главной сущности — реактивной переменной. Под ней мы понимаем объект, имеющий аксессоры к его единственному свойству. Аксессоры — это геттер и сеттер, функции, что заменяют собой получение и задание нужного нам свойства, разбавляя это действие своей логикой.
Наша реактивная переменная должна принимать аргумент при инициализации — стандартное значение. При вызове геттера (обычно, это метод .get без параметров) мы должны проверить, вызывается ли он внутри реактивного окружения, а затем записать нашу переменную в список отслеживаемых переменных. При вызове же сеттера (обычно это метод .set с параметром val — новое значение переменной) мы должны записать это значение в свойство объекта и обновить все зависимости.

var ReactiveVar = function(val) {   this.value = val; // инициализируем значение переменной по-умолчанию }; ReactiveVar.prototype.get = function() {   if(Tracker.active) { // мы внутри реактивного окружения     Deps.vars[Deps.vars.length - 1].push(this); // втыкаем переменную в стак для связи ее с трекером   }    return this.value; // возвращаем значение свойства }; ReactiveVar.prototype.set = function(val) {   this.value = val; // обновляем значение свойства    for(var i = 0; i < Deps.vars.length; i++) {     if(Deps.vars[i].indexOf(this) > -1) { // зависимость найдена       Deps.funcs[i](); // вызываем функцию окружения заново     }   }    return this.value; // возвращаем значение свойства }; 

Соберем все вместе и получим почти готовую библиотеку:

Заголовок спойлера

(function() {   var Deps = {     funcs: [],     vars: []   };    var Tracker = function(fn) {     Deps.funcs.push(fn);     Deps.vars.push([]);     Deps.tracker = true;      fn();      Deps.tracker = false;   };    var ReactiveVar = function(init) {     this.value = init;   };    ReactiveVar.prototype.get = function() {     if(Deps.tracker) {       Deps.vars[Deps.vars.length - 1].push(this);     }      return this.value;   };    ReactiveVar.prototype.set = function(val) {     var i = 0;     var self = this;     this.value = val;     Deps.vars.forEach(function(arr) {       if(arr.indexOf(self) > -1) {         Deps.funcs[i]();       }       i += 1;     });   };      // убираем window. для NodeJS приложения   window.Tracker = Tracker;   window.ReactiveVar = ReactiveVar; })(); 

Теперь давайте попробуем использовать реактивность в наших целях! Для этого реализуем самый простой секундомер. Нам потребуется 1 реактивная переменная и интервал, тикающий каждую секунду:

var time = new ReactiveVar(1); // инициализируем значение  Tracker(function() {   var curTime = time.get(); // начинаем отслеживание изменений переменной   document.querySelector('#time').innerHTML = curTime; // изменяем DOM в связи с изменением нашей переменной });  setInterval(function() {   time.set(time.get() + 1); }, 1000); 

Вот и все. Как мы смогли увидеть, реактивность в JS — это не что-то из разряда фантастики, а очень даже простая парадигма программирования.

ссылка на оригинал статьи http://habrahabr.ru/post/266081/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *