AngularJS: Предотвращение ошибки $digest уже выполняется при вызове $scope. $Apply()

Я нахожу, что мне нужно обновлять мою страницу до области видимости вручную все больше и больше с момента создания приложения в angular.

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

Ошибка: $digest уже выполняется

Кто-нибудь знает, как избежать этой ошибки или добиться того же, но по-другому?

785
задан Lightbulb1 04 окт. '12 в 17:07
источник поделиться
25 ответов

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

Вы можете проверить, выполняется ли $digest, проверив $scope.$$phase.

if(!$scope.$$phase) {
  //$digest or $apply
}

$scope.$$phase вернет "$digest" или "$apply", если выполняется $digest или $apply. Я считаю, что разница между этими состояниями заключается в том, что $digest будет обрабатывать часы текущего объема и его дочерних элементов, а $apply будет обрабатывать наблюдателей всех областей.

В точку @dnc253, если вы часто вызываете $digest или $apply, возможно, вы ошибаетесь. Обычно я нахожу, что мне нужно переварить, когда мне нужно обновить состояние области в результате запуска DOM-события вне досягаемости Angular. Например, когда твитер-бутстрап-модальный становится скрытым. Иногда событие DOM срабатывает, когда выполняется $digest, иногда нет. Вот почему я использую эту проверку.

Я хотел бы знать лучший способ, если кто-нибудь знает его.


Из комментариев: by @anddoutoi

angular.js Анти-шаблоны

  1. Не выполняйте if (!$scope.$$phase) $scope.$apply(), это означает, что ваш $scope.$apply() недостаточно высок в стеке вызовов.
636
ответ дан Lee 12 окт. '12 в 15:28
источник поделиться

Из недавнего обсуждения с ребятами Angular по этой теме: Для будущих причин не следует использовать $$phase

При нажатии на "правильный" способ сделать это, ответ в настоящее время

$timeout(function() {
  // anything you want can go here and will safely be run on the next digest.
})

Недавно я столкнулся с этим при написании сервисов Angular для обертывания API-интерфейсов facebook, google и twitter, которые в той или иной степени передавали обратные вызовы.

Вот пример из службы. (Для краткости остальная часть сервиса - настройка переменных, вложенных $timeout и т.д. - была остановлена.)

window.gapi.client.load('oauth2', 'v2', function() {
    var request = window.gapi.client.oauth2.userinfo.get();
    request.execute(function(response) {
        // This happens outside of angular land, so wrap it in a timeout 
        // with an implied apply and blammo, we're in action.
        $timeout(function() {
            if(typeof(response['error']) !== 'undefined'){
                // If the google api sent us an error, reject the promise.
                deferred.reject(response);
            }else{
                // Resolve the promise with the whole response if ok.
                deferred.resolve(response);
            }
        });
    });
});

Обратите внимание, что аргумент delay для $timeout не является обязательным и по умолчанию будет 0, если оставить unset ($timeout вызывает $browser.defer, который по умолчанию равен 0, если задержка не установлена ​​)

Немного неинтуитивно, но ответ от ребята, пишущего Angular, поэтому он достаточно хорош для меня!

631
ответ дан betaorbust 25 сент. '13 в 7:06
источник поделиться

Цикл дайджеста - это синхронный вызов. Это не приведет к управлению циклом событий браузера, пока он не будет выполнен. Есть несколько способов справиться с этим. Самый простой способ справиться с этим - использовать встроенный тайм-аут $, а второй - если вы используете подчеркивание или lodash (и вы должны быть), вызовите следующее:

$timeout(function(){
    //any code in here will automatically have an apply run afterwards
});

или если у вас есть символ подчеркивания:

_.defer(function(){$scope.$apply();});

Мы попробовали несколько обходных решений, и нам не хотелось вводить $rootScope во все наши контроллеры, директивы и даже некоторые фабрики. Таким образом, $timeout и _.defer были нашими фаворитами до сих пор. Эти методы успешно сообщают angular ждать следующего цикла анимации, который гарантирует, что текущая область применения $apply закончена.

312
ответ дан frosty 31 июля '13 в 1:51
источник поделиться

Многие из ответов здесь содержат хорошие советы, но также могут привести к путанице. Просто использование $timeout - не лучшее и правильное решение. Кроме того, обязательно прочтите, что, если вас беспокоит производительность или масштабируемость.

Вещи, которые вы должны знать

  • $$phase является приватным для фреймворка, и для этого есть веские причины.

  • $timeout(callback) будет ждать завершения текущего цикла дайджеста (если таковой имеется), затем выполнить обратный вызов, а затем запустить в конце полный $apply.

  • $timeout(callback, delay, false) сделает то же самое (с дополнительной задержкой перед выполнением обратного вызова), но не будет запускать $apply (третий аргумент), который сохраняет производительность, если вы не изменили модель Angular ($ масштаб).

  • $scope.$apply(callback) вызывает, помимо прочего, $rootScope.$digest, что означает, что он будет перенаправлять корневую область приложения и всех его дочерних элементов, даже если вы находитесь в изолированной области.

  • $scope.$digest() будет просто синхронизировать свою модель с представлением, но не будет переваривать область его родителей, что может сэкономить много результатов при работе над изолированной частью вашего HTML с изолированной областью (из директивы в основном). $digest не выполняет обратный вызов: вы выполняете код, затем перевариваете.

  • $scope.$evalAsync(callback) был введен с угловыми 1.2 и, вероятно, решит большинство ваших проблем. Пожалуйста, обратитесь к последнему абзацу, чтобы узнать больше об этом.

  • если вы получите $digest already in progress error, ваша архитектура неверна: вам не нужно перенастраивать вашу область действия, или вы не должны отвечать за нее (см. ниже).

Как структурировать код

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

function editModel() {
  $scope.someVar = someVal;
  /* Do not apply your scope here since we don't know if that
     function is called synchronously from Angular or from an
     asynchronous code */
}

// Processed by Angular, for instance called by a ng-click directive
$scope.applyModelSynchronously = function() {
  // No need to digest
  editModel();
}

// Any kind of asynchronous code, for instance a server request
callServer(function() {
  /* That code is not watched nor digested by Angular, thus we
     can safely $apply it */
  $scope.$apply(editModel);
});

И если вы знаете, что делаете, и работаете над изолированной небольшой директивой, входящей в большое приложение Angular, вы можете предпочесть $digest вместо $apply для сохранения производительности.

Обновление с Angularjs 1.2

В любой $scope добавлен новый мощный метод: $evalAsync. В принципе, он выполнит свой обратный вызов в текущем цикле дайджеста, если он произойдет, иначе новый цикл дайджеста начнет выполнение обратного вызова.

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

В этих случаях и всех других, где у вас есть !$scope.$$phase, обязательно используйте $scope.$evalAsync( callback )

258
ответ дан floribon 16 апр. '14 в 9:59
источник поделиться

Удобный небольшой вспомогательный метод для поддержания этого процесса DRY:

function safeApply(scope, fn) {
    (scope.$$phase || scope.$root.$$phase) ? fn() : scope.$apply(fn);
}
86
ответ дан lambinator 14 июня '13 в 21:14
источник поделиться

У меня была такая же проблема с сценариями сторонних разработчиков, как например CodeMirror и Krpano, и даже используя методы safeApply, упомянутые здесь, не решили ошибку для меня.

Но для чего он решил использовать $timeout service (не забудьте сначала ввести его).

Таким образом, что-то вроде:

$timeout(function() {
  // run my code safely here
})

и если внутри вашего кода вы используете

это

возможно потому, что он внутри контроллера директивы factory или просто нуждается в каком-то привязке, тогда вы бы сделали что-то вроде:

.factory('myClass', [
  '$timeout',
  function($timeout) {

    var myClass = function() {};

    myClass.prototype.surprise = function() {
      // Do something suprising! :D
    };

    myClass.prototype.beAmazing = function() {
      // Here 'this' referes to the current instance of myClass

      $timeout(angular.bind(this, function() {
          // Run my code safely here and this is not undefined but
          // the same as outside of this anonymous function
          this.surprise();
       }));
    }

    return new myClass();

  }]
)
31
ответ дан Ciul 03 сент. '13 в 3:15
источник поделиться

См. http://docs.angularjs.org/error/$rootScope:inprog

Проблема возникает, если у вас есть вызов $apply, который иногда запускается асинхронно вне кода Angular (когда применяется $apply), а иногда синхронно внутри кода Angular (что вызывает ошибку $digest already in progress).

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

Способ предотвращения этой ошибки заключается в обеспечении того, чтобы код, вызывающий $apply, запускался асинхронно. Это можно сделать, выполнив код внутри вызова $timeout с задержкой, установленной на 0 (которая является значением по умолчанию). Однако вызов вашего кода внутри $timeout устраняет необходимость вызова $apply, потому что $timeout инициирует другой цикл $digest сам по себе, который, в свою очередь, сделает все необходимое обновление и т.д.

Решение

Короче говоря, вместо этого:

... your controller code...

$http.get('some/url', function(data){
    $scope.$apply(function(){
        $scope.mydate = data.mydata;
    });
});

... more of your controller code...

сделайте следующее:

... your controller code...

$http.get('some/url', function(data){
    $timeout(function(){
        $scope.mydate = data.mydata;
    });
});

... more of your controller code...

Вызов $apply только тогда, когда вы знаете, что запущенный код всегда будет выполняться вне кода Angular (например, ваш вызов $apply произойдет внутри обратного вызова, вызываемого кодом вне вашего кода Angular)..

Если кто-то не знает о каком-то значительном недостатке использования $timeout over $apply, я не понимаю, почему вы не всегда могли использовать $timeout (с нулевой задержкой) вместо $apply, так как это будет примерно то же самое.

31
ответ дан Trevor 22 янв. '14 в 0:33
источник поделиться

Когда вы получаете эту ошибку, это в основном означает, что она уже находится в процессе обновления вашего представления. Вам действительно не нужно вызывать $apply() в вашем контроллере. Если ваше представление не обновляется, как вы ожидали, а затем вы получите эту ошибку после вызова $apply(), это скорее всего означает, что вы не обновляете модель правильно. Если вы опубликуете некоторые особенности, мы можем выяснить основную проблему.

28
ответ дан dnc253 04 окт. '12 в 17:41
источник поделиться

Самая короткая форма безопасного $apply:

$timeout(angular.noop)
14
ответ дан Warlock 13 нояб. '14 в 16:00
источник поделиться

Вы также можете использовать evalAsync. Он будет запущен через некоторое время после завершения дайджеста!

scope.evalAsync(function(scope){
    //use the scope...
});
11
ответ дан CMCDragonkai 19 сент. '13 в 4:48
источник поделиться

Иногда вы будете получать ошибки, если используете этот способ (qaru.site/questions/12065/...).

Попробуйте следующее:

if(! $rootScope.$root.$$phase) {
...
9
ответ дан bullgare 30 апр. '13 в 16:24
источник поделиться

Прежде всего, не исправляйте это так

if ( ! $scope.$$phase) { 
  $scope.$apply(); 
}

Это не имеет смысла, потому что $ phase - это просто логический флаг цикла $ digest, поэтому ваш $ apply() иногда не запускается. И помните, что это плохая практика.

Вместо этого используйте $timeout

    $timeout(function(){ 
  // Any code in here will automatically have an $scope.apply() run afterwards 
$scope.myvar = newValue; 
  // And it just works! 
});

Если вы используете подчеркивание или lodash, вы можете использовать defer():

_.defer(function(){ 
  $scope.$apply(); 
});
6
ответ дан M Sagar 05 марта '18 в 11:25
источник поделиться

Вы должны использовать $evalAsync или $timeout в соответствии с контекстом.

Это ссылка с хорошим объяснением:

http://www.bennadel.com/blog/2605-scope-evalasync-vs-timeout-in-angularjs.htm

5
ответ дан Luc 22 июля '14 в 15:05
источник поделиться

Я бы посоветовал вам использовать настраиваемое событие, а не запускать цикл дайджеста.

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

Создавая настраиваемое событие, вы также более эффективно выполняете свой код, потому что вы вызываете только прослушиватели, подписавшиеся на указанное событие, и НЕ запускаете все часы, привязанные к области, как если бы вы вызывали область. $apply.

$scope.$on('customEventName', function (optionalCustomEventArguments) {
   //TODO: Respond to event
});


$scope.$broadcast('customEventName', optionalCustomEventArguments);
4
ответ дан nelsonomuto 02 янв. '14 в 18:26
источник поделиться

yearofmoo проделал отличную работу по созданию для нас функции повторного использования $safeApply:

https://github.com/yearofmoo/AngularJS-Scope.SafeApply

Использование:

//use by itself
$scope.$safeApply();

//tell it which scope to update
$scope.$safeApply($scope);
$scope.$safeApply($anotherScope);

//pass in an update function that gets called when the digest is going on...
$scope.$safeApply(function() {

});

//pass in both a scope and a function
$scope.$safeApply($anotherScope,function() {

});

//call it on the rootScope
$rootScope.$safeApply();
$rootScope.$safeApply($rootScope);
$rootScope.$safeApply($scope);
$rootScope.$safeApply($scope, fn);
$rootScope.$safeApply(fn);
3
ответ дан RNobel 21 нояб. '13 в 16:33
источник поделиться

используйте $scope.$$phase || $scope.$apply(); вместо

2
ответ дан Visakh B Sujathan 21 мая '15 в 14:27
источник поделиться

попробуйте использовать

$scope.applyAsync(function() {
    // your code
});

вместо

if(!$scope.$$phase) {
  //$digest or $apply
}

$ applyAsync Запланируйте вызов $ apply, чтобы произойти в более позднее время. Это можно использовать для очереди нескольких выражений, которые необходимо оценивать в одном и том же дайджесте.

ПРИМЕЧАНИЕ. В $ digest $ applyAsync() будет только скрываться, если текущая область действия - это $ rootScope. Это означает, что если вы вызовете $ digest в дочерней области, он не будет явно скрывать очередь $ applyAsync().

Exmaple:

  $scope.$applyAsync(function () {
                if (!authService.authenticated) {
                    return;
                }

                if (vm.file !== null) {
                    loadService.setState(SignWizardStates.SIGN);
                } else {
                    loadService.setState(SignWizardStates.UPLOAD_FILE);
                }
            });

Рекомендации:

1. Scope. $ ApplyAsync() vs. Scope. $ EvalAsync() в AngularJS 1.3

  1. AngularJs Docs
2
ответ дан Eduardo Eljaiek 12 марта '18 в 19:22
источник поделиться

Я смог решить эту проблему, вызвав $eval вместо $apply в местах, где я знаю, что будет выполняться функция $digest.

В соответствии с docs, $apply в основном делает следующее:

function $apply(expr) {
  try {
    return $eval(expr);
  } catch (e) {
    $exceptionHandler(e);
  } finally {
    $root.$digest();
  }
}

В моем случае, ng-click изменяет переменную в пределах области действия, а $watch на этой переменной меняет другие переменные, которые должны быть $applied. Этот последний шаг приводит к тому, что ошибка "дайджест уже выполняется".

Заменив $apply на $eval внутри выражения watch, переменные области обновляются, как ожидалось.

Следовательно, кажется, что если дайджест будет работать в любом случае из-за какого-либо другого изменения внутри Angular, $eval 'ing все, что вам нужно сделать.

2
ответ дан teleclimber 02 сент. '13 в 0:20
источник поделиться

Понимая, что документы Angular вызывают проверку $$phase a anti-pattern, я попытался получить $timeout и _.defer для работы.

Тайм-аут и отложенные методы создают вспышку нераспакованного содержимого {{myVar}} в dom, как FOUT. Для меня это было неприемлемо. Это оставляет меня без особого догматического подтверждения, что что-то является взломом и не имеет подходящей альтернативы.

Единственное, что работает каждый раз:

if(scope.$$phase !== '$digest'){ scope.$digest() }.

Я не понимаю опасности этого метода или почему он описывается как взлом людей в комментариях и команде Angular. Команда кажется точной и легкой для чтения:

"Сделайте дайджест, если он уже не происходит"

В CoffeeScript он еще красивее:

scope.$digest() unless scope.$$phase is '$digest'

В чем проблема? Есть ли альтернатива, которая не создаст FOUT? $safeApply выглядит отлично, но также использует метод проверки $$phase.

1
ответ дан SimplGy 31 дек. '14 в 22:29
источник поделиться

Это мой сервис utils:

angular.module('myApp', []).service('Utils', function Utils($timeout) {
    var Super = this;

    this.doWhenReady = function(scope, callback, args) {
        if(!scope.$$phase) {
            if (args instanceof Array)
                callback.apply(scope, Array.prototype.slice.call(args))
            else
                callback();
        }
        else {
            $timeout(function() {
                Super.doWhenReady(scope, callback, args);
            }, 250);
        }
    };
});

и это пример использования:

angular.module('myApp').controller('MyCtrl', function ($scope, Utils) {
    $scope.foo = function() {
        // some code here . . .
    };

    Utils.doWhenReady($scope, $scope.foo);

    $scope.fooWithParams = function(p1, p2) {
        // some code here . . .
    };

    Utils.doWhenReady($scope, $scope.fooWithParams, ['value1', 'value2']);
};
1
ответ дан ranbuch 24 авг. '15 в 8:59
источник поделиться

Я использую этот метод, и он работает отлично. Это просто ждет завершения цикла, а затем вызывает apply(). Просто вызовите функцию apply(<your scope>) из любой точки.

function apply(scope) {
  if (!scope.$$phase && !scope.$root.$$phase) {
    scope.$apply();
    console.log("Scope Apply Done !!");
  } 
  else {
    console.log("Scheduling Apply after 200ms digest cycle already in progress");
    setTimeout(function() {
        apply(scope)
    }, 200);
  }
}
1
ответ дан Ashu 10 мая '16 в 17:23
источник поделиться

Вы можете использовать

$timeout

чтобы предотвратить ошибку.

 $timeout(function () {
                        var scope = angular.element($("#myController")).scope();
                        scope.myMethod();
                        scope.$scope();
                    },1);
0
ответ дан Satish Singh 15 сент. '17 в 13:54
источник поделиться

похоже на ответы выше, но это верно сработало для меня... в службе добавьте:

    //sometimes you need to refresh scope, use this to prevent conflict
    this.applyAsNeeded = function (scope) {
        if (!scope.$$phase) {
            scope.$apply();
        }
    };
0
ответ дан Shawn Dotey 12 янв. '16 в 23:39
источник поделиться

Нашел это: https://coderwall.com/p/ngisma, где Натан Уокер (около нижней части страницы) предлагает декоратору в $rootScope создать func 'safeApply', код:

yourAwesomeModule.config([
  '$provide', function($provide) {
    return $provide.decorator('$rootScope', [
      '$delegate', function($delegate) {
        $delegate.safeApply = function(fn) {
          var phase = $delegate.$$phase;
          if (phase === "$apply" || phase === "$digest") {
            if (fn && typeof fn === 'function') {
              fn();
            }
          } else {
            $delegate.$apply(fn);
          }
        };
        return $delegate;
      }
    ]);
  }
]);
-3
ответ дан Warren Davis 11 апр. '14 в 23:00
источник поделиться

Это решит вашу проблему:

if(!$scope.$$phase) {
  //TODO
}
-7
ответ дан user4536971 11 февр. '15 в 22:10
источник поделиться

Другие вопросы по меткам или Задайте вопрос