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

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

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

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

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

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

Вы можете проверить, выполняется ли $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

Анти-Шаблоны углов.

  1. Не if (!$scope.$$phase) $scope.$apply() , это означает вашу $scope.$apply() недостаточно высока в стеке вызовов.

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

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

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

Я недавно столкнулся с этим при написании угловых сервисов, чтобы обернуть 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); } }); }); }); 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  • $scope.$evalAsync(callback) был введен с angularjs 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); }); 

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

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

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

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

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

Удобный маленький вспомогательный метод для поддержания этого процесса СУХОЙ:

 function safeApply(scope, fn) { (scope.$$phase || scope.$root.$$phase) ? fn() : scope.$apply(fn); } 

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

Проблема возникает, когда у вас есть вызов $apply который иногда запускается асинхронно вне углового кода (когда применяется $ apply), а иногда синхронно внутри Углового кода (что вызывает ошибку $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 только $apply когда вы знаете, что запущенный код всегда будет работать за пределами углового кода (например, ваш вызов $ apply произойдет внутри обратного вызова, вызываемого кодом вне вашего углового кода).

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

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

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

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

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

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

это

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

 .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(); }] ) - .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(); }] ) 

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

Самая короткая форма безопасного использования:

 $timeout(angular.noop) 

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

 scope.evalAsync(function(scope){ //use the scope... }); 

Иногда вы будете получать ошибки, если используете этот способ ( https://stackoverflow.com/a/12859093/801426 ).

Попробуй это:

 if(! $rootScope.$root.$$phase) { ... 

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

 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(); }); 

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

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

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

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

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

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

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

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); 

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

Согласно документам , $apply основном делает это:

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

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

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

Поэтому представляется, что если дайджест будет работать в любом случае из-за какого-либо другого изменения в пределах Angular, $eval ‘ing – это все, что вам нужно сделать.

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

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

 $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

Понимая, что Угловые документы требуют проверки $$phase на анти-шаблон , я пытался получить $timeout и _.defer для работы.

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

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

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

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

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

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

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

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

Это мое служебное обслуживание:

 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']); }; 

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

 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); } } 

подобно ответам выше, но это верно сработало для меня … в сервисе добавьте:

  //sometimes you need to refresh scope, use this to prevent conflict this.applyAsNeeded = function (scope) { if (!scope.$$phase) { scope.$apply(); } }; 

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

$timeout

для предотвращения ошибки.

  $timeout(function () { var scope = angular.element($("#myController")).scope(); scope.myMethod(); scope.$scope(); },1); 

Нашел это: 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; } ]); } ]); 

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

 if(!$scope.$$phase) { //TODO } 
Давайте будем гением компьютера.