Почему используется if (! $Scope. $$ phase) $scope. $Apply() анти-шаблон?

Иногда мне нужно использовать $scope.$apply в моем коде, и иногда он выдает ошибку "digest is in progress". Итак, я начал искать способ и нашел этот вопрос: AngularJS: Предотвращение ошибки $digest уже выполняется при вызове $scope. $Apply(). Однако в комментариях (и на wiki-странице angular) вы можете прочитать:

Не выполняйте if (! $scope. $$ phase) $scope. $apply(), это означает вашу область $. $apply() недостаточно высок в стеке вызовов.

Итак, у меня есть два вопроса:

  • Почему именно это анти-шаблон?
  • Как можно безопасно использовать $scope. $apply?

Еще одно "решение" для предотвращения ошибки "дайджест уже выполняется", похоже, использует $timeout:

$timeout(function() {
  //...
});

Это путь? Это безопаснее? Итак, вот реальный вопрос: как я могу полностью устранить возможность ошибки "дайджест уже в процессе"?

PS: Я использую $scope. $применяются в не-угловых обратных вызовах, которые не являются синхронными. (насколько я знаю, это ситуации, когда вы должны использовать $scope. $apply, если вы хотите, чтобы ваши изменения были применены)

Ответ 1

После некоторого дополнительного копания я смог решить вопрос, всегда ли безопасно использовать $scope.$apply. Короткий ответ - да.

Длинный ответ:

Из-за того, как ваш браузер выполняет Javascript, невозможно, чтобы два вызова дайджеста сталкивались случайно.

Код JavaScript, который мы пишем, не запускается за один раз, а выполняется по очереди. Каждый из этих поворотов работает без передышки от начала до конца, а когда очередь работает, в нашем браузере больше ничего не происходит. (от http://jimhoskins.com/2012/12/17/angularjs-and-apply.html)

Следовательно, ошибка "дайджест уже выполняется" может произойти только в одной ситуации: когда $apply выдается внутри другого $apply, например:

$scope.apply(function() {
  // some code...
  $scope.apply(function() { ... });
});

В этой ситуации может возникнуть не , если мы используем $scope.apply в чистом обращении без учета углов, например, обратный вызов setTimeout. Таким образом, следующий код на 100% пуленепробиваемый и нет нужно сделать if (!$scope.$$phase) $scope.$apply()

setTimeout(function () {
    $scope.$apply(function () {
        $scope.message = "Timeout called!";
    });
}, 2000);

даже этот безопасен:

$scope.$apply(function () {
    setTimeout(function () {
        $scope.$apply(function () {
            $scope.message = "Timeout called!";
        });
    }, 2000);
});

Что такое НЕ безопасно (поскольку $timeout - как и все помощники angularjs - уже звонит $scope.$apply для вас):

$timeout(function () {
    $scope.$apply(function () {
        $scope.message = "Timeout called!";
    });
}, 2000);

Это также объясняет, почему использование if (!$scope.$$phase) $scope.$apply() является анти-шаблоном. Вы просто не нуждаетесь в этом, если используете $scope.$apply правильно: в чистом js-обратном вызове, например setTimeout.

Подробнее читайте http://jimhoskins.com/2012/12/17/angularjs-and-apply.html.

Ответ 2

В любом случае, когда ваш дайджест выполняется, и вы нажимаете другую услугу для переваривания, она просто дает ошибку, то есть дайджест уже выполняется. поэтому, чтобы вылечить это, у вас есть два варианта. вы можете проверить, есть ли какой-либо другой дайджест, как опрос.

Первый

if ($scope.$root.$$phase != '$apply' && $scope.$root.$$phase != '$digest') {
    $scope.$apply();
}

если указанное выше условие истинно, тогда вы можете применить свою область $. $apply otherwies not и

второе решение использует $timeout

$timeout(function() {
  //...
})

он не позволит другому дайджесту начинаться до истечения $timeout завершения выполнения.

Ответ 3

Это определенно анти-шаблон сейчас. Я видел, как дайджест взорвался, даже если вы проверяете фазу $$. Вы просто не должны обращаться к внутреннему API, обозначенному префиксами $$.

Вы должны использовать

 $scope.$evalAsync();

так как это предпочтительный метод в Angular ^ 1.4 и специально показан как API для прикладного уровня.

Ответ 4

scope.$apply запускает цикл $digest, который является основополагающим для привязки двухсторонних данных

A $digest проверяет цикл для объектов, т.е. модели (точнее, $watch), прикрепленные к $scope, чтобы оценить, изменились ли их значения, и если он обнаруживает изменение, то он принимает необходимые шаги для обновления представления.

Теперь, когда вы используете $scope.$apply, вы сталкиваетесь с ошибкой "Уже в процессе" , поэтому вполне очевидно, что работает $digest, но что вызвало его?

ans → каждый вызов $http, все ng-click, repeat, show, hide etc запускают цикл $digest И НАСТОЯЩАЯ ЧАСТЬ, ЧТОБЫ ИСПОЛЬЗУЕТЕ КАЖДОЙ $SCOPE.

Например, ваша страница имеет 4 контроллера или директивы A, B, C, D

Если у вас есть 4 $scope свойства в каждом из них, то у вас на вашей странице есть все свойства области $16.

Если вы запускаете $scope.$apply в контроллере D, тогда цикл $digest будет проверять все 16 значений!!! плюс все свойства $rootScope.

Ответ → , но $scope.$digest запускает a $digest для дочернего и одного объекта, поэтому он будет проверять только 4 свойства. Поэтому, если вы уверены, что изменения в D не повлияют на A, B, C, используйте $scope.$diges t not $scope.$apply.

Таким образом, простой ng-click или ng-show/hide может запускать цикл $digest для более чем 100 свойств, даже если у пользователя не было никакого события!

Ответ 5

Используйте $timeout, это рекомендуется.

Мой сценарий заключается в том, что мне нужно изменить элементы на странице, основываясь на данных, полученных из WebSocket. И поскольку он находится вне Angular, без $timeout, только модель будет изменена, но не вид. Поскольку Angular не знает, что часть данных была изменена. $timeout в основном говорит Angular, чтобы внести изменения в следующий раунд $digest.

Я тоже пробовал следующее: он работает. Разница в том, что $timeout более ясный.

setTimeout(function(){
    $scope.$apply(function(){
        // changes
    });
},0)

Ответ 6

Я нашел очень классное решение:

.factory('safeApply', [function($rootScope) {
    return function($scope, fn) {
        var phase = $scope.$root.$$phase;
        if (phase == '$apply' || phase == '$digest') {
            if (fn) {
                $scope.$eval(fn);
            }
        } else {
            if (fn) {
                $scope.$apply(fn);
            } else {
                $scope.$apply();
            }
        }
    }
}])

введите, где вам нужно:

.controller('MyCtrl', ['$scope', 'safeApply',
    function($scope, safeApply) {
        safeApply($scope); // no function passed in
        safeApply($scope, function() { // passing a function in
        });
    }
])