Я уже создавал упрощенную модель внедрения зависимостей AngularJS, теперь решил тоже проделать с циклом дайджета.
Именно всемогущий дайджет и решает работу всех вотчеров, а так как двойное связывание строится на вотчерах, то он вообще все решает.
Место действия $rootScope
, а еще точнее – Scope.prototype
. Именно там определяется метод $digest
.
Постановка задачи
Попробуем сформулировать условие задачи: нам необходимо реализовать такой метод digest
, который проверит все наблюдаемые переменные и, если значение было изменено, вызовет колбэк.
Дополнительно нам нужно договориться как мы будем определять/задавать этим самые наблюдаемые переменные. Допустим мы сделаем это тоже через метод watch
, как это делает AngularJS.
Итого имеем:
[javascript]
var rootScope = function () {};
rootScope.prototype = {
watch: function (watchExp, listener) {},
digest: function () {}
}
[/javascript]
(я специально не использую $ перед rootScope
, watch
и digest
, чтобы как-то разделить реализации AngularJS и мою)
Ну и соотвественно вариант использования подразумевает быть таким:
[javascript]
var scope = new rootScope();
scope.x = 5;
scope.watch(‘x’, function(newValue, oldValue){ alert(‘changed:’ + oldValue + ‘->’ + newValue) });
scope.x = 10;
scope.digest();
[/javascript]
этот код должен вывести алерт со значениями.
Реализация
Займемся методом watch
. У него четкое назначение – создавать “вотчеры” – то есть объекты, которые будут в себе хранить информацию о наблюдаемом объекте.
Чтобы хранить где-то эти вотчеры создадим специальное свойство-массив у объекта rootScope
:
[javascript]
var rootScope = function () {
this.watchers = [];
};
[/javascript]
Теперь при каждом вызове метода watch
можем записывать новый объект в этот массив:
[javascript]
…
watch: function (watchExp, listener) {
var watcher = {
watchExp : watchExp,
listener : listener || function() {},
lastValue: this[watchExp],
};
this.watchers.push(watcher);
},
…
[/javascript]
C методом watch
разобрались, теперь перейдем к методу digest
. По идее этот метод должен пробегать по всем вотчерам и смотреть не изменились ли они. А если изменились – вызывать их обработчики:
[javascript]
digest: function () {
var scope = this;
this.watchers.forEach(function(watcher){
if(watcher.lastValue !== scope[watcher.watchExp]){
watcher.listener.call(scope, scope[watcher.watchExp], watcher.lastValue);
}
});
}
[/javascript]
и еще, в случае когда значение обновилось, нам нужно сохранить новое значение:
[javascript]
digest: function () {
var scope = this;
this.watchers.forEach(function(watcher){
if(watcher.lastValue !== scope[watcher.watchExp]){
watcher.listener.call(scope, scope[watcher.watchExp], watcher.lastValue);
watcher.lastValue = scope[watcher.watchExp];
}
});
}
[/javascript]
Как будто бы все. Но нет – мы забыли еще добавить механизм “грязной проверки”, которая позволяет убедиться в том, что мы “ни о ком не забыли”. Уточнение: при выполнении обработчиков вотчеров мы могли изменить значения других наблюдаемых объектов, то есть снова их сделали “грязными”.
Чтобы это учесть введем специальную переменную – индикатор статуса dirty
:
[javascript]
digest: function () {
var scope = this,
dirty = false;
this.watchers.forEach(function(watcher){
if(watcher.lastValue !== scope[watcher.watchExp]){
watcher.listener.call(scope, scope[watcher.watchExp], watcher.lastValue);
watcher.lastValue = scope[watcher.watchExp];
dirty = true;
}
});
return dirty;
}
[/javascript]
теперь завернем всю нашу логику в метод digestOnce
и будем выполнять его до тех пор, пока не “почистим” все вотчеры:
[javascript]
digest: function () {
var dirty;
do {
dirty = digestOnce(this);
} while (dirty);
function digestOnce(scope) {
var dirty = false;
scope.watchers.forEach(function (watcher) {
if (watcher.lastValue !== scope[watcher.watchExp]) {
watcher.listener.call(scope, scope[watcher.watchExp], watcher.lastValue);
watcher.lastValue = scope[watcher.watchExp];
dirty = true;
}
});
return dirty;
}
}
[/javascript]
Ну вот и все: упрощенная модель дайджеста готова. С кодом можно поиграться тут.
Бонус
Во избежание зацикливания дайджеста( например: в случае циклических зависимостей) мы можем ограничить количество проходов c помощью специальной переменной. В AngularJS эту переменную назвали TTL
(вероятно от аббревиатуры “time to live”):
[javascript]
digest: function () {
var dirty,
ttl = 10;
do {
dirty = digestOnce(this);
if (dirty && !(ttl–)) {
throw "10 digest iterations reached";
}
} while (dirty);
…
[/javascript]
Также в AngularJS есть возможность снятия вотчера путем выполнения функции возвращаемой из метода $watch
. Реализуем эту функциональность сохранив синтаксис AngularJS. Для этого нам нужно удалить наш вотчер из массива вотчеров:
[javascript]
watch: function (watchExp, listener) {
var watcher = {
watchExp: watchExp,
listener: listener,
lastValue: this[watchExp],
}, scope = this;
this.watchers.push(watcher);
return function unwatch(){
scope.watchers.splice(scope.watchers.indexOf(watcher), 1);
};
}
[/javascript]
Ну и еще раз весь код.
P.S.
Крайне рекомендую поковыряться в исходниках AngularJS, вот тут. Я вот внезапно обнаружил, что у $watch есть еще 4-тый недокументированный параметр prettyPrintExpression.