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

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

    Similar posts

    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 19

      0
      Рекомендую почитать: habrahabr.ru/post/235121
        0
        Большое спасибо! Я думал там что-то более из ряда вон выходящее. А модели в ангуляре по такому же принципу работают? Или двойные биндинги по другому работают?
          0
          Модели ($scope) в AngularJS работают через dirty-checking. Это одна из причин, почему его так любят лишний раз пнуть.
          В Angular2 будет поддержка реактивности и иммутабельных данных.
            0
            На самом деле в Метеоре всё несколько сложнее. Например, автозапускаемые функции исполняются не немедленно, а отложенно.
              0
              Да, но целью я ставил показать новичкам, что реактивность — это просто. В будущих статьях разберем реализацию реактивности так, как она сделана в метеоре от и до.
                0
                В таком случае её можно было бы реализовать ещё проще. Как в KnockoutJS, например.
                  0
                  Так и где обещанное? =))
                    0
                    Будет :) Пока времени на все не хватает.
              0
              Следующей статьей хотелось бы увидеть на примере этого же фреймворка как устроено локальное хранение коллекций, как и где они храниться?!)
                +1
                Не более чем в памяти (сервера или/и клиента — смотря где созданы) за это отвечает minimongo
                +1
                А что такое хипстерский по отношению к фреймворку?
                  0
                  Фреймворк для хипстеров. Это как, например, программистский.
                  0
                  Блин, смотрю в код, понимаю код, а понять принцип не могу, парадигма ломке не поддаётся… Либо я тупой, либо надо отдохнуть…
                    +1
                    Смотри, у нас есть некие отслеживаемые данные и соответствующие действия, которые будут выполнены при изменении этих данных. При изменении данных, благодаря зависимостям возникает «волна» изменяющихся данных, которые переводят всю систему из одного состояния в другое, которое есть результат выполнения некоторых действий, например, выполнение формулы в табличном процессоре (Excel, Calc), расчет ипотечного кредита на сайте банка при каждом движении ползунка или изменении поля для ввода суммы кредита и т.п.

                    В данном случае в статье функция передаваемая в Tracker описывает то, какой процесс будет выполнен. При этом самое первое выполнение Tracker'а формирует эту динамическую систему (реактивное окружение), которое раскладывает все отслеживаемые данные и «соответствующие действия» по своим местам. И затем уже вызов метода set реактивной переменной с некоторым новым значением инициирует «волну» пересчета ипотечного кредита :) Как-то так.
                      +1
                      Тут главное быть осторожным и не писать в трекере что-то типа:

                      isOpened.set( !isOpened.get() )
                      

                      А то бесконечный цикл вам обеспечен :-)
                        0
                        Для этого можно дописать условие :)
                          0
                          Какое?
                            0
                            Например, такое:
                            // in ReactiveVar.prototype.set
                            if(Deps.vars[Deps.vars.length - 1].indexOf(this) > -1) {
                              console.error('Error: Infinity loop');
                              return;
                            }
                            

                            Это не сработает на вложенных трекерах, но мы их и не рассматривали здесь. Таким образом, если в текущем окружении мы уже отслеживаем изменение переменной, то отменяем ее обновление и показываем соответствующее сообщение для разработчика.
                              0
                              Ну ок, для одного трекера проблему решили. А что будем делать с пинг-понгом между трекерами?

                  Only users with full accounts can post comments. Log in, please.