Продвинутая реактивность

Отступление 11.5

На данный момент переведено

В этой главе вы:

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

    Представьте, что мы хотим выяснить, сколько друзей в Facebook текущего пользователя лайкнули каждый пост на Microscope. Допустим, что мы уже проработали все детали аутентификации пользователя через Facebook, добавили необходимые вызовы API и получили интересующие нас данные. Теперь у нас на клиенте есть асинхронная функция, которая возвращает количество лайков, - getFacebookLikeCount(user, url, callback).

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

    Чтобы это исправить, мы можем начать с использования setInterval и вызывать нашу функцию каждые несколько секунд:

    currentLikeCount = 0;
    Meteor.setInterval(function() {
      var postId;
      if (Meteor.user() && postId = Session.get('currentPostId')) {
        getFacebookLikeCount(Meteor.user(), Posts.find(postId).url,
          function(err, count) {
            if (!err)
              currentLikeCount = count;
          });
      }
    }, 5 * 1000);
    

    Каждый раз, когда мы проверяем переменную currentLikeCount, мы ожидаем получить правильное число для конкретного момента с ошибкой в пределах пяти секунд. Теперь мы можем использовать данную переменную в хелпере следующим образом:

    Template.postItem.likeCount = function() {
      return currentLikeCount;
    }
    

    Тем не менее, ничто пока не вызывает повторную отрисовку шаблона в случае изменения currentLikeCount. Хотя переменная теперь имитирует режим реального времени в том смысле, что она изменяется сама по себе, она не является реактивной и по-прежнему не может правильно взаимодействовать с остальной экосистемой Meteor.

    Отслеживаем реактивность: вычисления

    Реактивность Meteor опосредована зависимостями (dependencies) - структурами данных, которые отслеживают набор вычислений.

    Как мы уже видели в главе про реактивность, вычисление - это часть кода, которая использует реактивные данные. В нашем случае существует вычисление, которое было создано исключительно для шаблона postItem, и у каждого хелпера в менеджере этого шаблона тoже есть свое вычисление.

    Вы можете воспринимать вычисление как участок кода, который “заботится” о реактивных данных. В случае изменений в данных именно вычисление будет об этом проинформировано (при помощи invalidate()), и оно же будет решать, нужно ли предпринимать какие-либо действия.

    Превращаем переменную в реактивную функцию

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

    var _currentLikeCount = 0;
    var _currentLikeCountListeners = new Tracker.Dependency();
    
    currentLikeCount = function() {
      _currentLikeCountListeners.depend();
      return _currentLikeCount;
    }
    
    Meteor.setInterval(function() {
      var postId;
      if (Meteor.user() && postId = Session.get('currentPostId')) {
        getFacebookLikeCount(Meteor.user(), Posts.find(postId), 
          function(err, count) {
            if (!err && count !== _currentLikeCount) {
              _currentLikeCount = count;
              _currentLikeCountListeners.changed();
            }
          });
      }
    }, 5 * 1000);
    

    Мы только что установили зависимость _currentLikeCountListeners, которая следит за всеми вычислениями, использующими currentLikeCount(). Когда значение currentLikeCount() изменяется, мы вызываем функцию changed() для этой зависимости, что инвалидирует все наблюдаемые вычисления.

    Затем вычисления могут работать с изменением, рассматривая различные случаи по отдельности.

    Если вам кажется, что это большое количество шаблонного кода для простого реактивного источника данных, то вы правы, и в Meteor есть встроенные инструменты, позволяющие упростить процесс (точно так же как вместо того, чтобы использовать вычисления напрямую, вы обычно просто применяете autorun). Существует базовый пакет reactive-var, который делает абсолютно то же самое, что и функция currentLikeCount(). Если мы добавим его:

    meteor add reactive-var
    

    Мы можем использовать его, чтобы немного упростить код:

    var currentLikeCount = new ReactiveVar();
    
    Meteor.setInterval(function() {
      var postId;
      if (Meteor.user() && postId = Session.get('currentPostId')) {
        getFacebookLikeCount(Meteor.user(), Posts.find(postId),
          function(err, count) {
            if (!err) {
              currentLikeCount.set(count);
            }
          });
      }
    }, 5 * 1000);
    

    Теперь мы будем вызывать currentLikeCount.get() из хелпера, и все будет работать как прежде. Кроме этого существует базовый пакет reactive-dict, предоставляющий реактивное хранилище для пар “ключ-значение” (почти как Session), который тоже может быть полезным.

    Сравниваем Tracker и Angular

    Angular - это работающая только на клиентской стороне библиотека для реактивной отрисовки, разработанная ребятами из Google. Очень пояснительным является сравнение подходов Meteor и Angular к отслеживанию зависимостей, так как их различия существенны.

    Мы знаем, что модель Meteor использует блоки кода, называемые вычислениями. Эти вычисления отслеживаются специальными “реактивными” источниками данных (функциями), которые заботятся об их инвалидации в случае необходимости. Таким образом, источник данных явно информирует все свои зависимости, когда им нужно вызвать invalidate(). Имейте в виду, что, хотя это обычно происходит в случае изменения данных, потенциально инвалидация также может быть запущена источником данных по иным причинам.

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

    В Angular реактивность опосредована объектом scope (область видимости). Область видимости может рассматриваться как простой объект JavaScript с парой специальных методов.

    Когда вы хотите установить реактивную зависимость от значения в области видимости, вы вызываете scope.$watch, передавая выражение, которое вас интересует (к примеру, какие части области видимости нужно отслеживать), а также функцию-слушатель (англ. listener function), которая будет выполняться каждый раз, когда выражение будет изменяться. Таким образом, мы явно устанавливаем, что конкретно мы хотим делать в случае изменения значения выражения.

    Возвращаясь назад к нашему примеру с Facebook, мы бы написали:

    $rootScope.$watch('currentLikeCount', function(likeCount) {
      console.log('Current like count is ' + likeCount);
    });
    

    Конечно, так же как в Meteor вы редко определяете вычисления, в Angular вы не так уж часто явно вызываете $watch, потому что {{expressions}} и директивы ng-model автоматически устанавливают наблюдатели (англ. watchers), которые потом заботятся о перерисовке в случае изменений.

    Когда подобное реактивное значение изменяется, должна быть вызвана функция scope.$apply(). Она заново оценивает каждый наблюдатель области видимости, но вызывает функции-слушатели только для тех, значения вычислений которых изменились.

    Таким образом, scope.$apply() похожа на dependency.changed() за исключением того, что она действует на уровне области видимости, а не предоставляет вам право указать, какие точно функции-слушатели должны быть вычислены заново. Этот незначительный дефицит контроля позволяет Angular самому очень разумно и эффективно определять такие функции.

    С Angular наша функция getFacebookLikeCount() выглядела бы примерно так:

    Meteor.setInterval(function() {
      getFacebookLikeCount(Meteor.user(), Posts.find(postId), 
        function(err, count) {
          if (!err) {
            $rootScope.currentLikeCount = count;
            $rootScope.$apply();
          }
        });
    }, 5 * 1000);
    

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