forked from DiscoverMeteor/DiscoverMeteor_Ru
-
Notifications
You must be signed in to change notification settings - Fork 0
/
11s-advanced-reactivity.md.erb
152 lines (113 loc) · 13.7 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
---
title: Advanced Reactivity
title: Продвинутая реактивность
slug: advanced-reactivity
date: 0011/01/02
number: 11.5
sidebar: true
contents: Научитесь создавать реактивные источники данных в Meteor.| Создадите простой пример реактивного источника данных.| Увидите, в чем сходства и различия между Tracker и AngularJS.
paragraphs: 29
---
Ситуации, в которых необходимо самостоятельно писать код для отслеживания зависимостей, встречаются редко. Однако такой код, без сомнения, полезно понимать, чтобы следить за процессом разрешения зависимостей.
Представьте, что мы хотим выяснить, сколько друзей в Facebook текущего пользователя лайкнули каждый пост на Microscope. Допустим, что мы уже проработали все детали аутентификации пользователя через Facebook, добавили необходимые вызовы API и получили интересующие нас данные. Теперь у нас на клиенте есть асинхронная функция, которая возвращает количество лайков, - `getFacebookLikeCount(user, url, callback)`.
Важно помнить, что данная функция является *не реактивной* и не выполняется в реальном времени. Она будет посылать HTTP запрос к Facebook, получать данные и делать их доступными приложению через асинхронный коллбек. Однако функция не будет самостоятельно перезапускаться, когда изменится количество лайков на Facebook, а интерфейс не станет реагировать на изменения данных, лежащих в его основе.
Чтобы это исправить, мы можем начать с использования `setInterval` и вызывать нашу функцию каждые несколько секунд:
~~~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);
~~~
Каждый раз, когда мы проверяем переменную `currentLikeCount`, мы ожидаем получить правильное число для конкретного момента с ошибкой в пределах пяти секунд. Теперь мы можем использовать данную переменную в хелпере следующим образом:
~~~js
Template.postItem.likeCount = function() {
return currentLikeCount;
}
~~~
Тем не менее, ничто пока не вызывает повторную отрисовку шаблона в случае изменения `currentLikeCount`. Хотя переменная теперь имитирует режим реального времени в том смысле, что она изменяется сама по себе, она не является *реактивной* и по-прежнему не может правильно взаимодействовать с остальной экосистемой Meteor.
### Отслеживаем реактивность: вычисления
Реактивность Meteor опосредована *зависимостями (dependencies)* - структурами данных, которые отслеживают набор вычислений.
Как мы уже видели в главе про реактивность, вычисление - это часть кода, которая использует реактивные данные. В нашем случае существует вычисление, которое было создано исключительно для шаблона `postItem`, и у каждого хелпера в менеджере этого шаблона тoже есть свое вычисление.
Вы можете воспринимать вычисление как участок кода, который "заботится" о реактивных данных. В случае изменений в данных именно вычисление будет об этом проинформировано (при помощи `invalidate()`), и оно же будет решать, нужно ли предпринимать какие-либо действия.
### Превращаем переменную в реактивную функцию
Чтобы превратить нашу переменную `currentLikeCount` в реактивный источник данных, нам нужно отслеживать все вычисления, которые ее используют в рамках зависимости. Это предполагает ее превращение из переменной в функцию, возвращающую значение:
~~~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" %>
Мы только что установили зависимость `_currentLikeCountListeners`, которая следит за всеми вычислениями, использующими `currentLikeCount()`. Когда значение `currentLikeCount()` изменяется, мы вызываем функцию `changed()` для этой зависимости, что инвалидирует все наблюдаемые вычисления.
Затем вычисления могут работать с изменением, рассматривая различные случаи по отдельности.
Если вам кажется, что это большое количество шаблонного кода для простого реактивного источника данных, то вы правы, и в Meteor есть встроенные инструменты, позволяющие упростить процесс (точно так же как вместо того, чтобы использовать вычисления напрямую, вы обычно просто применяете autorun). Существует базовый пакет `reactive-var`, который делает абсолютно то же самое, что и функция `currentLikeCount()`. Если мы добавим его:
~~~bash
meteor add reactive-var
~~~
Мы можем использовать его, чтобы немного упростить код:
~~~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" %>
Теперь мы будем вызывать `currentLikeCount.get()` из хелпера, и все будет работать как прежде. Кроме этого существует базовый пакет `reactive-dict`, предоставляющий реактивное хранилище для пар "ключ-значение" (почти как `Session`), который тоже может быть полезным.
### Сравниваем Tracker и Angular
[Angular](http://angularjs.org/) - это работающая только на клиентской стороне библиотека для реактивной отрисовки, разработанная ребятами из Google. Очень пояснительным является сравнение подходов Meteor и Angular к отслеживанию зависимостей, так как их различия существенны.
Мы знаем, что модель Meteor использует блоки кода, называемые вычислениями. Эти вычисления отслеживаются специальными "реактивными" источниками данных (функциями), которые заботятся об их инвалидации в случае необходимости. Таким образом, источник данных _явно_ информирует все свои зависимости, когда им нужно вызвать `invalidate()`. Имейте в виду, что, хотя это обычно происходит в случае изменения данных, потенциально инвалидация также может быть запущена источником данных по иным причинам.
Кроме того, хотя вычисления обычно перезапускаются только в результате инвалидации, вы можете настроить их по своему желанию. Все это дает нам высокий уровень контроля над реактивностью.
В Angular реактивность опосредована объектом `scope (область видимости)`. Область видимости может рассматриваться как простой объект JavaScript с парой специальных методов.
Когда вы хотите установить реактивную зависимость от значения в области видимости, вы вызываете `scope.$watch`, передавая выражение, которое вас интересует (к примеру, какие части области видимости нужно отслеживать), а также функцию-слушатель (англ. listener function), которая будет выполняться каждый раз, когда выражение будет изменяться. Таким образом, мы явно устанавливаем, что конкретно мы хотим делать в случае изменения значения выражения.
Возвращаясь назад к нашему примеру с Facebook, мы бы написали:
~~~js
$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()` выглядела бы примерно так:
~~~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" %>
Стоит признать, что Meteor берет на себя большую часть тяжелой работы и позволяет использовать реактивность без особых усилий с нашей стороны. Однако мы надеемся, что изучение этих моделей будет полезным, если когда-нибудь вам придется работать над чем-то более сложным.