AngularJS: Предотвращение ошибки $digest уже выполняется при вызове $scope. $Apply()
Я нахожу, что мне нужно обновлять мою страницу до области видимости вручную все больше и больше с момента создания приложения в angular.
Единственный способ, которым я знаю это сделать, - вызвать $apply()
из области моих контроллеров и директив. Проблема заключается в том, что он продолжает бросать ошибку на консоль, которая читает:
Ошибка: $digest уже выполняется
Кто-нибудь знает, как избежать этой ошибки или добиться того же, но по-другому?
Ответы
Ответ 1
Не используйте этот шаблон - это приведет к большему количеству ошибок, чем решит. Даже если вы думаете, что это что-то исправило, это не так.
Вы можете проверить, выполняется ли уже $digest
, проверив $scope.$$phase
.
if(!$scope.$$phase) {
//$digest or $apply
}
$scope.$$phase
вернет "$digest"
или "$apply"
, если выполняется $digest
или $apply
. Я полагаю, что разница между этими состояниями заключается в том, что $digest
будет обрабатывать часы текущей области и ее дочерних элементов, а $apply
будет обрабатывать наблюдатели всех областей.
На @dnc253, если вы часто звоните $digest
или $apply
, возможно, вы делаете это неправильно. Я обычно нахожу, что мне нужно переварить, когда мне нужно обновить состояние области видимости в результате события DOM, запускаемого за пределами досягаемости Angular. Например, когда модал начальной загрузки Twitter становится скрытым. Иногда событие DOM срабатывает, когда $digest
выполняется, иногда нет. Вот почему я использую этот чек.
Я хотел бы узнать лучший способ, если кто-нибудь знает один.
Из комментариев:
@anddoutoi
Angular.js Anti Patterns
-
Не делайте
if (!$scope.$$phase) $scope.$apply()
, это означает, что ваш $scope.$apply()
недостаточно высок в стеке вызовов.
Ответ 2
Из недавнего обсуждения с ребятами 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, поэтому он достаточно хорош для меня!
Ответ 3
Цикл дайджеста - это синхронный вызов. Это не даст контроль над циклом событий браузера, пока это не будет сделано. Есть несколько способов справиться с этим. Самый простой способ справиться с этим - использовать встроенный тайм-аут в $ timeout, а во-вторых, если вы используете подчеркивание или lodash (и вам следует), вызовите следующее:
$timeout(function(){
//any code in here will automatically have an apply run afterwards
});
или если у вас есть lodash:
_.defer(function(){$scope.$apply();});
Мы попробовали несколько обходных путей и ненавидели внедрять $ rootScope во все наши контроллеры, директивы и даже некоторые фабрики. Итак, $ timeout и _.defer были нашими любимыми до сих пор. Эти методы успешно сообщают angular ждать следующего цикла анимации, что гарантирует завершение текущей области видимости. $ Apply.
Ответ 4
Многие из ответов здесь содержат хорошие советы, но также могут привести к путанице. Просто использование $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 )
Ответ 5
Удобный небольшой вспомогательный метод для поддержания этого процесса DRY:
function safeApply(scope, fn) {
(scope.$$phase || scope.$root.$$phase) ? fn() : scope.$apply(fn);
}
Ответ 6
У меня была такая же проблема с сценариями сторонних разработчиков, как например 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();
}]
)
Ответ 7
См. 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
, так как это будет примерно то же самое.
Ответ 8
Когда вы получаете эту ошибку, это в основном означает, что она уже находится в процессе обновления вашего представления. Вам действительно не нужно вызывать $apply()
в вашем контроллере. Если ваше представление не обновляется, как вы ожидали, а затем вы получите эту ошибку после вызова $apply()
, это скорее всего означает, что вы не обновляете модель правильно. Если вы опубликуете некоторые особенности, мы можем выяснить основную проблему.
Ответ 9
Самая короткая форма безопасного $apply
:
$timeout(angular.noop)
Ответ 10
Вы также можете использовать evalAsync. Он будет запущен через некоторое время после завершения дайджеста!
scope.evalAsync(function(scope){
//use the scope...
});
Ответ 11
Прежде всего, не исправляйте это так
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();
});
Ответ 12
Иногда вы будете получать ошибки, если используете этот способ (fooobar.com/questions/12065/...).
Попробуйте следующее:
if(! $rootScope.$root.$$phase) {
...
Ответ 13
Вы должны использовать $evalAsync или $timeout в соответствии с контекстом.
Это ссылка с хорошим объяснением:
http://www.bennadel.com/blog/2605-scope-evalasync-vs-timeout-in-angularjs.htm
Ответ 14
Я бы посоветовал вам использовать настраиваемое событие, а не запускать цикл дайджеста.
Я пришел к выводу, что трансляция пользовательских событий и регистрация слушателей для этих событий - хорошее решение для запуска действия, которое вы хотите выполнить, независимо от того, находитесь ли вы в цикле дайджеста.
Создавая настраиваемое событие, вы также более эффективно выполняете свой код, потому что вы вызываете только прослушиватели, подписавшиеся на указанное событие, и НЕ запускаете все часы, привязанные к области, как если бы вы вызывали область. $apply.
$scope.$on('customEventName', function (optionalCustomEventArguments) {
//TODO: Respond to event
});
$scope.$broadcast('customEventName', optionalCustomEventArguments);
Ответ 15
попробуйте использовать
$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
- AngularJs Docs
Ответ 16
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);
Ответ 17
Я смог решить эту проблему, вызвав $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 все, что вам нужно сделать.
Ответ 18
используйте $scope.$$phase || $scope.$apply();
вместо
Ответ 19
Понимая, что документы 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
.
Ответ 20
Это мой сервис 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']);
};
Ответ 21
Я использую этот метод, и он работает отлично. Это просто ждет завершения цикла, а затем вызывает 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);
}
}
Ответ 22
Когда я отключил отладчик, ошибка больше не происходит. В моем случае это было из-за того, что отладчик остановил выполнение кода.
Ответ 23
похоже на ответы выше, но это верно сработало для меня...
в службе добавьте:
//sometimes you need to refresh scope, use this to prevent conflict
this.applyAsNeeded = function (scope) {
if (!scope.$$phase) {
scope.$apply();
}
};
Ответ 24
Вы можете использовать
$timeout
чтобы предотвратить ошибку.
$timeout(function () {
var scope = angular.element($("#myController")).scope();
scope.myMethod();
scope.$scope();
},1);
Ответ 25
Проблема в основном наступает, когда мы просим angular запустить цикл дайджеста, даже если он находится в процессе, что создает проблему в angular to понимание. исключение следствия в консоли.
1. Нет смысла вызывать scope. $ Apply() внутри функции $ timeout, потому что внутренне это делает то же самое.
2. Код идет с ванильной функцией JavaScript, потому что его нативный, не angular, angular определен, т.е. setTimeout
3. Для этого вы можете использовать
если (! Scope. $$ фаза) {
Объем. $ evalAsync (функция() {
}); }
Ответ 26
Нашел это: 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;
}
]);
}
]);
Ответ 27
Это решит вашу проблему:
if(!$scope.$$phase) {
//TODO
}