AngularJS под капотом: $apply() и $digest()

| Пятница, 29 августа, 2014

Метки AngularJS


Функции $apply() и $digest() являются основным ядром AngularJS и часто самыми непонятными. Чтобы понять как функционирует AngularJS, нужно разобраться как работают $apply() и $digest(). Данная статья расскажет, чем являются $apply() и $digest(), и как они могут пригодиться для практического программирования.

Кто такие $apply() и $digest()?

AngularJS обладает замечательным свойством, которое называется двухстороннее связывание данных и это сильно упрощает разработку. Связывание данных означает, что если вы измените что-то в представлении, то scope-модель автоматически обновится. Аналогично, где бы не были изменены данные в scope-модели, представление сразу же будет обновлено в соответствии с изменениями данных в модели. Как AngularJS делает это? Допустим, вы написали выражение в представлении {{ aModel }}, то за кулисами AngularJS создаст функцию, которая будет отслеживать изменения scope-модели. То есть, изменена модель, сразу обновляется представление. И эта функция идентична любой "наблюдающей" функции (watcher), которые вы создаете в своих приложениях:

$scope.$watch('aModel', function(newValue, oldValue) { 
  // обновляем DOM новым значением newValue 
});

Вторым аргументом в watcher-функции передается "слушающая" функция (listener), которая вызывается всякий раз, когда значение aModel изменится. Итак, уясним, изменяется значение aModel, вызывается listener-функция, которая обновляет выражение в HTML. Но есть один большой вопрос! Как AngularJS узнает, что нужно вызвать listener-функцию. Другими словами, как AngularJS понимает, что значение aModel изменилось, и нужно вызвать соответствующую listener-функцию. Может быть он периодически запрашивает значения модели scope и так определяет их изменения? И тут на сцену выходит наш первый персонаж - цикл $digest().

Внутри цикла $digest() вызываются watcher-функции, и когда вызвана очередная watcher-функция, AngularJS проверяет scope-модель и если значение изменилось, то вызывается соответствующая listener-функция. Наш следующий вопрос, где и когда вызывается этот цикл $digest()?

Цикл $digest выполняется вызовом $scope.$digest(). Предположим, вы изменили значение scope-модели в каком-нибудь обработчике, переданном в "родную", встроенную директиву AngularJS, типа ng-click. В этом случае AngularJS автоматически инициирует вызов функции $digest(). Когда цикл $digest запустится, он вызовет все watcher-функции. Каждый watcher проверит свою модель на предмет изменения с момента последней проверки, и если значения разные, то вызовется соответствующая listener-функция. И в результате все выражения в представлении будут изменены этими listeners. В дополнении к ng-click AngularJS имеет множество встроенных директив/сервисов, которые позволяют изменять модели (ng-model, $timeout, и т.д.) и автоматически вызывают цикл $digest.

Вроде все складно... Но Angular не так прост. Среда AngularJS не вызывает $digest напрямую, она вызывает функцию $scope.$apply(), которая вызывает $rootScope.digest(). И в результате digest-цикл в $rootScope поочередно посещает дочерние $scope и вызывает их digest-циклы.

Теперь , предположим, вы прикрепили директиву ng-click к кнопке и передали функцию в директиву для обработки нажатия этой кнопки. После нажатия кнопки, Angular вызывает обработчик внутри $scope.$apply(), и из-за этого после выполнения обработчика клика стартует $digest, который передает изменения в представление.

Примечание. Функция $scope.$apply() автоматически вызывает $rootScope.digest(). Реализована $apply() в двух версиях. Первая принимает в качестве аргумента функцию-обработчик, выполняет ее и вызывает цикл $digest. Вторая ничего не принимает, а просто вызывает $digest(). Зачем это нужно, рассмотрим позже.