Reactividad avanzada

Sidebar 11.5

Porcentaje completado

En este capítulo:

  • Aprenderemos a crear fuentes de datos reactivas.
  • Crearemos un sencillo ejemplo de una fuente de datos reactiva.
  • Compararemos Tracker con AngularJS.
  • No es común tener que escribir código de seguimiento de dependencias por ti mismo, pero para comprender el concepto, es verdaderamente útil seguir el camino de cómo funciona el flujo de dependencias.

    Imagina que quisiéramos saber a cuántos amigos del usuario actual de Facebook le ha gustado cada post en Microscope. Supongamos que ya hemos trabajado en los detalles de cómo autenticar el usuario con Facebook, hacer las llamadas necesarias a la API, y procesar los datos relevantes. Ahora tenemos una función asíncrona en el lado del cliente que devuelve el número de “me gusta”: getFacebookLikeCount(user, url, callback).

    Lo importante a recordar sobre una función de esta naturaleza es que no es reactiva ni funciona en tiempo real. Hará una petición HTTP a Facebook, enviando algunos datos, y obtendremos el resultado en la aplicación a través de una llamada asíncrona. Pero la función no se va a volver a ejecutar por sí sola cuando haya un cambio en Facebook, ni nuestra UI va a cambiar cuando los datos lo hagan.

    Para solucionar esto, podemos comenzar utilizando setInterval para llamar a la función cada ciertos segundos:

    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);
    

    Cada vez que usemos esa variable currentLikeCount, obtendremos el número correcto con un margen de error de cinco segundos. Ahora podemos usar esa variable en un ayudante:

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

    Sin embargo, nada le dice todavía a nuestra plantilla que se redibuje cuando cambie currentLikeCount. Si bien la variable ahora está en pseudo tiempo real (se cambia a sí misma), no es reactiva y por lo tanto todavía no puede comunicarse correctamente con el resto del ecosistema de Meteor.

    Rastreando la Reactividad: Computaciones

    La reactividad de Meteor es mediada por dependencias, estructuras de datos que rastrean una serie de computaciones.

    Como vimos anteriormente, una computación es una parte de código que usa datos reactivos. En nuestro caso, hay una computación que ha sido implícitamente creada por la plantilla postItem, y cada ayudante en el manejador de esa plantilla está funcionando dentro de esa computación.

    Puedes pensar de la computación como una parte del código que se “preocupa” por los datos reactivos. Cuando los datos cambien, está computación será informada (a través de invalidate()), y es la computación la que debe decidir si algo debe hacerse.

    Trasformando una Variable en una Función Reactiva

    Para trasformar nuestra variable currentLikeCount en una fuente de datos reactiva, necesitamos rastrear todas las computaciones que la usan como una dependencia. Esto requiere trasformarla de una variable a una función (que devolverá un valor):

    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);
    

    Lo que hemos hecho es configurar una dependencia _currentLikeCountListeners, que rastreará todas las computaciones en las cuales se utilice currentLikeCount(). Cuando el valor de _currentLikeCount cambie, podemos llamar a la función changed() en esa dependencia, que invalida todas las computaciones realizadas.

    Estas computaciones pueden entonces seguir adelante y evaluar los cambios caso por caso.

    Parece un montón de código para una única fuente de datos reactiva, y tienes razón, por lo que Meteor proporciona algunas herramientas de serie para hacerlo un poco más sencillo. (normalmente, no se usan computaciones directamente, si no sencillamente auto ejecuciones). Hay un paquete llamado reactive-var que hace exactamente lo que hacemos con la función currentLikeCount(). Por lo que si lo añadimos:

    meteor add reactive-var
    

    Podremos simplificar nuestro código un poco:

    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);
    

    Ahora para usarlo, llamaremos a currentLikeCount.get() en nuestro ayudante y debería funcionar como antes. Hay también otro paquete reactive-dict, que proporciona un almacén de datos clave-valor reactivo (exactamente igual que la Session), que podría ser útil también.

    Comparando Tracker con Angular

    Angular es una librería de renderizado reactivo del lado del cliente, desarrollada por la buena gente de Google. Solo se puede comparar el seguimiento de dependencias de Meteor con el de Angular de un modo ilustrativo, ya que sus enfoques son bastante diferentes.

    Hemos visto que el modelo de Meteor usa bloques de código llamados computaciones. Estas son seguidas por fuentes de datos “reactivas” (funciones) que se ocupan de invalidarlos cuando sea apropiado. Así, la fuente de datos informa explícitamente todas sus dependencias cuando necesita llamar a invalidate(). Nótese que a pesar de que esto sucede generalmente cuando los datos cambian, la fuente de los datos puede además decidir ejecutar la invalidación por otras razones.

    Además, por más que usualmente las computaciones simplemente se reejecutan cuando son invalidadas, se pueden configurar para que se comporten como uno quiera. Esto nos da un alto nivel de control sobre la reactividad.

    En Angular, la reactividad es medida por el objeto scope. Un scope, o alcance, puede ser pensado como un simple objeto de JavaScript con algunos métodos especiales.

    Cuando se desea depender reactivamente de un valor dentro del scope, se llama a scope.$watch, declarando la expresión en la que uno está interesado (por ejemplo, qué partes del scope te importan) y una función que se ejecutará cada vez que esa expresión cambie. Así, se puede declarar explícitamente qué hacer cada vez que ese valor sea modificado.

    Volviendo a nuestro ejemplo con Facebook, escribiríamos:

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

    Por supuesto, es tan raro tener que configurar computaciones en Meteor, como tener que invocar a $watch explícitamente en Angular, ya que las directivas ng-model y las {{expressions}} automáticamente se ocupan de re-renderizarse cuando haya un cambio.

    Cuando dicho valor reactivo sea cambiado, scope.$apply() debe ser llamado. Esto vuelve a evaluar cada “watcher” del scope, pero solo llama a las funciones de aquellos “watchers” que contengan valores modificados.

    Entonces, scope.$apply() es similar a dependency.changed(), excepto que actúa al nivel del scope, en lugar de darle el control al desarrollador para decirle precisamente cuáles funciones deberían ser re-evaluadas. Con eso aclarado, esta pequeña falta de control le da a Angular la habilidad de ser muy inteligente y eficiente, ya que determina precisamente qué debe ser vuelto a evaluar.

    Con Angular, nuestra función getFacebookLikeCount() habría sigo algo así:

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

    Decididamente, Meteor se ocupa de la parte más pesada por nosotros y nos deja beneficiarnos de la reactividad sin demasiado trabajo. Pero tal vez, aprender estos patrones será de ayuda si alguna vez necesitas ir más allá.