-
Notifications
You must be signed in to change notification settings - Fork 30
/
11s-advanced-reactivity.md.erb
155 lines (115 loc) · 8.6 KB
/
11s-advanced-reactivity.md.erb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
---
title: Reactividad avanzada
slug: advanced-reactivity
date: 0011/01/02
number: 11.5
points: 10
sidebar: true
photoUrl: http://www.flickr.com/photos/ikewinski/8676146109/
photoAuthor: Mike Lewinski
contents: Aprenderemos a crear fuentes de datos reactivas.|Crearemos un sencillo ejemplo de una fuente de datos reactiva.|Compararemos Tracker con AngularJS.
paragraphs: 29
---
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:
~~~js
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:
~~~js
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):
~~~js
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);
~~~
<%= highlight "1~7,14~17" %>
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:
~~~bash
meteor add reactive-var
~~~
Podremos simplificar nuestro código un poco:
~~~js
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);
~~~
<%= highlight "1,9" %>
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](http://angularjs.org/) 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:
~~~js
$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í:
~~~js
Meteor.setInterval(function() {
getFacebookLikeCount(Meteor.user(), Posts.find(postId),
function(err, count) {
if (!err) {
$rootScope.currentLikeCount = count;
$rootScope.$apply();
}
});
}, 5 * 1000);
~~~
<%= highlight "5~6" %>
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á.