Как стать автором
Поиск
Написать публикацию
Обновить

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

Время на прочтение4 мин
Количество просмотров18K

Доброе время суток, хабраюзер! Сегодня мы попытаемся немного разобрать реактивность, которая лежит в основе одного из самых хипстерских фреймворков — 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); // так как мы находимся вне реактивного окружения, то не подписываемся на события обновления переменной time, однако, обновляем все зависимости этой переменной
}, 1000);

Вот и все. Как мы смогли увидеть, реактивность в JS — это не что-то из разряда фантастики, а очень даже простая парадигма программирования.
Теги:
Хабы:
Всего голосов 15: ↑12 и ↓3+9
Комментарии19

Публикации

Ближайшие события