Link, $observe и $watch функции в директивах, выполняемые в контексте AngularJS

в 8:24, , рубрики: AngularJS, digest, javascript, Веб-разработка, директивы

При запуске своего кода внутри контроллера или сервиса не приходится беспокоиться о вызове $apply(), поскольку код выполняется внутри контекста Ангуляра. Под этим подразумевается, что Ангуляр понимает, что ваш код находится в процессе выполнения и выполнит грязную проверку (dirty-check) после завершения его работы. Когда же вы находитесь внутри директивы, мировоззрение Ангуляра чуть более ограничено; теперь директива должна заботиться о вызове $apply() (или вызове $apply() с чем-то вроде $timeout), когда необходимо сообщить Ангуляру об изменениях в модели представления (т. е. $scope). Однако, определить когда это нужно делать, немного сложнее, потому что некоторые аспекты директивы фактически выполняются внутри контекста Ангуляра.

Если вы уже создавали свои собственные директивы, можно не сомневаться, что видели одно из двух сообщений:

$apply is already in progress.
$digest is already in progress.

Эти сообщения об ошибках указывают, что вы пытаетесь сказать Ангуляру о коде, который он уже знает. В лучшем случае, просто увидите эту ошибку в консоли. В худшем — возникнет рекурсивное фиаско, которое сломает браузер (один из немногих недостатков Firebug).

Скорее всего вы получили эту ошибку, потому что находились внутри директивы, и пытались сказать Ангуляру обо всех изменениях в $scope, которые произошли внутри директивы. И, хотя философия ваших действий верна, существуют части директивы, которые Ангуляр уже мониторил. В частности, Ангуляр уже знает о:

— связующей функции
— обработчиках $observe() для атрибутов
— обработчиках $watch() для $scope

Если вы вносите изменения в $scope внутри кода, выполняемого синхронно в связующей функции или асинхронно внутри $observe() и $watch() обработчиков, то действия уже выполняются в контексте Ангуляра. Это означает, что Ангуляр выполнит грязную проверку после выполнения вашего кода и вызовет дополнительные $digest циклы, если необходимо.

Чтобы продемонстрировать это, я написал небольшую директиву, которая отслеживает событие загрузки изображения. Если во время выполнения директивы изображение уже загружено, обработчик вызывается сразу — в контексте Ангуляра. Если изображение еще не загружено, обработчик вызывается асинхронно и необходимо явно уведомить Ангуляр об изменениях.

Примечание: Обработчики $observe() и $watch() показаны в демонстрационных целях. Они ничего не делают.

Код примера

<!doctype html>
<html ng-app="Demo">
<head>
    <meta charset="utf-8" />
</head>
<body>
    <ul ng-controller="ListController">

        <!-- У этих изображений динамические src-атрибуты -->
        <li ng-repeat="image in images">
            <p>Loaded: {{ image.complete }}</p>
            <img ng-src="{{ image.source }}" bn-load="imageLoaded( image )"  />
        </li>
 
        <!-- У этого изображения статический src-атрибут -->
        <li>
            <p>Loaded: {{ staticImage.complete }}</p>
            <img src="4.png" bn-load="imageLoaded( staticImage )" />
        </li>
    </ul>
 
    <!-- Загрузка jQuery и AngularJS -->
    <script type="text/javascript" src="//code.jquery.com/jquery-1.9.0.min.js"></script>
    <script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/angularjs/1.0.4/angular.min.js"></script>
    <script type="text/javascript">

        // Создаем модуль приложения
        var Demo = angular.module( "Demo", [] ); 
 
        // Контроллер для списка изображений
        Demo.controller("ListController", function( $scope ) {
 
                // Помечаем изображения как загруженные
                $scope.imageLoaded = function( image ) {
                    image.complete = true;
                };
 
                // Настройка изображений коллекции. Поскольку все эти изображения
                // загружаются через data-URIs, они будут загружены немедленно
                $scope.images = [{
                        complete: false,
                        source: "1.png"
                    }, {
                        complete: false,
                        source: "2.png"
                    },  {
                        complete: false,
                        source: "3.png"
                    }];
 
                // Пример статичного изображения
                $scope.staticImage = {
                    complete: false,
                    source: "4.png"
                };
            });
 
        // Выражение выполняется, когда текущее изображение будет загружено
        Demo.directive( "bnLoad", function() {
 
                // Связываем события DOM с областью видимости.
                function link( $scope, element, attributes ) {
 
                    // Выражение выполняется в текущем цикле
                    // $digest и нет необходимости вызывать $apply()
                    function handleLoadSync() {
                        logWithPhase( "handleLoad - Sync" );
                        $scope.$eval( attributes.bnLoad );
                    }
 
                    // Выполнение выражения и последующий
                    // вызов $digest, чтобы Ангуляр смог узнать
                    // что изменение произошло
                    function handleLoadAsync() {
                        logWithPhase( "handleLoad - Async" );
                        $scope.$apply( function() {
                             handleLoadSync();
                        });
                    }
 
                    // Записываем в лог фазу жизненного цикла текущей области видимости.
                    function logWithPhase( message ) {
                         console.log( message, ":", $scope.$$phase );
                    }
 
                    // Проверяем, было ли изображение уже загружено.
                    // Если изображение взято из кэша браузера
                    // или загружено как Data URI,
                    // то не будет никаких задержек до окончания загрузки
                    if ( element[0].src && element[0].complete ) {
                        handleLoadSync();

                    // Изображение будет загружено в какой-то момент в будущем
                    // (т.е. асинхронно в связующей функции).
                    } else {
                        element.on( "load.bnLoad", handleLoadAsync );
                    }
  
                    // В демонстрационных целях давайте понаблюдаем за
                    // интерполированныеми атрибутами, чтобы увидеть
                    // в какой фазе находится область видимости
                    attributes.$observe("src", function( srcAttribute ) {
                        logWithPhase( "$observe : " + srcAttribute );
                    });
 
                    // В демонстрационных целях давайте понаблюдаем
                    // за изменениями значения изображения после окончания загрузки.
                    // Примечание: директива НЕ должна знать об этом значении модели;
                    // но мы ознакомимся здесь с жизненным циклом.
                    $scope.$watch("( image || staticImage ).complete", function( newValue ) {
                        logWithPhase( "$watch : " + newValue );
                    });
 
                    // Очищаем после уничтожения области видимости
                    $scope.$on("$destroy", function() {
                        element.off( "load.bnLoad" );
                    });
                }
 
                // Возвращаем конфигурацию директивы
                return({
                    link: link,
                    restrict: "A"
                });
            }
        );
    </script>
</body>
</html>

Как можно видеть, три изображения (внутри ngRepeat) загружаются динамически и одно — статически. Загрузка всех четырех изображений мониторится с помощью директивы bnLoad и фазы цикла $digest записываются в лог.

Статическое изображение — 4.png — ко времени выполнения директивы будет уже загружено и послужит причиной первого срабатывания обработчика, который затем выведет в консоль следующее:

handleLoad - Sync : $apply
$observe : 4.png : $digest
$watch : true : $digest
$observe : 1.png : $digest
$watch : false : $digest
$observe : 2.png : $digest
$watch : false : $digest
$observe : 3.png : $digest
$watch : false : $digest
$observe : 1.png : $digest
$observe : 2.png : $digest
$observe : 3.png : $digest
handleLoad - Async : null
handleLoad - Sync : $apply
$watch : true : $digest
handleLoad - Async : null
handleLoad - Sync : $apply 
$watch : true : $digest
handleLoad - Async : null
handleLoad - Sync : $apply
$watch : true : $digest

Знаю, что, непонятно с какой стороны подступиться, поэтому укажу несколько ключевых пунктов:

Первое срабатывание handleLoadSync() было вызвано статическим изображением. Обратите внимание, что оно произошло уже внутри фазы $apply, в которой Ангуляр проводит грязную проверку. Это потому, что она была вызвана в связующей функции link(), которая уже находится в контексте Ангуляра.

Все $observe() и $watch() обработчики находятся внутри фазы $digest грязной проверки жизненного цикла Ангуляра. Однажды попав туда, не нужно будет говорить Ангуляру о любых изменениях в $scope. Ангуляр будет автоматически выполнять грязную проверку после каждого цикла $digest.

Все изображения, загружаемые асинхронно, вызвали метод handleLoadAsync(), который использует метод $apply() для того, чтобы сообщить Ангуляру об этих изменениях. Именно поэтому все последующие методы handleLoadSync() находятся в фазе $apply — они были вызваны из обработчика handleLoadAsync().

Как упоминал ранее, директивы Ангуляра являются очень мощными, но с подвывертом и придется попыхтеть, чтобы поставить голову на место. Время выполнения в директиве Ангуляра имеет решающее значение для его функциональности, и это одна из наиболее трудных вещей для правильного понимания. Добавьте что-то вроде CSS-переходов — которые имеют только частичную поддержку браузерами — и вы быстро заметите, что проблемы с $digest в одном браузере возникают, а в другом — нет. Надеемся, что это исследование поможет немного лучше разобраться в причине подобных проблем.

Автор: tamtakoe

Источник

Поделиться

* - обязательные к заполнению поля