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