AngularJS:防止错误$digest已经在进行时调用$范围。

我发现,自从以角度构建应用程序以来,我越来越需要手动更新我的页面到我的范围。

我知道的唯一方法是从我的控制器和指令的范围内调用$apply()。这样做的问题是它不断向控制台抛出一个错误,内容如下:

错误:$digest已在进行中

有没有人知道如何避免这个错误或以不同的方式实现同样的事情?

460207 次浏览

当你收到此错误时,基本上意味着它已经在更新你的视图的过程中。你真的不需要在控制器中调用$apply()。如果你的视图没有像你预期的那样更新,然后你在调用$apply()后收到此错误,这很可能意味着你没有正确更新模型。如果你发布一些细节,我们可以找出核心问题。

不要用这个图案-这最终会导致比它解决的更多的错误。即使你认为它修复了一些东西,它没有。

您可以通过检查$scope.$$phase来检查$digest是否已经在进行中。

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

如果$digest$apply正在进行,$scope.$$phase将返回"$digest""$apply"。我相信这些状态之间的区别在于$digest将处理当前范围及其子级的监视程序,而$apply将处理所有范围的监视程序。

针对@dnc253的观点,如果你发现自己经常调用$digest$apply,那么你可能做错了。我通常发现,当我需要更新范围的状态时,我需要消化一下,因为DOM事件在Angular范围之外触发。例如,当twitter引导模式变得隐藏时。有时DOM事件在$digest正在进行时触发,有时没有。这就是我使用这个检查的原因。

我想知道一个更好的方法,如果有人知道一个。


评论:著者:@张国荣

angular.js反模式

  1. 不要做if (!$scope.$$phase) $scope.$apply(),这意味着你的$scope.$apply()在调用堆栈中不够高。

如果您使用这种方式,有时您仍然会收到错误(https://stackoverflow.com/a/12859093/801426)。

试试这个:

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

方便的小助手方法来保持这个过程DRY:

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

摘要循环是一个同步调用。在完成之前,它不会将控制权交给浏览器的事件循环。有几种方法可以解决这个问题。最简单的方法是使用内置的$timeout,第二种方法是如果你使用下划线或洛达什(你应该是),调用以下命令:

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

如果你有洋葱:

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

我们尝试了几种变通方法,我们讨厌将$rootScope注入到我们所有的控制器、指令甚至一些工厂中。因此,$timeout和_. deler是我们迄今为止最喜欢的。这些方法成功地告诉角等待下一个动画循环,这将保证当前范围.$应用结束。

我已经能够通过在我知道$digest函数将运行的地方调用$eval而不是$apply来解决这个问题。

根据文档$apply基本上是这样做的:

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

在我的例子中,ng-click更改了范围内的变量,并且该变量上的$watch更改了必须为$applied的其他变量。最后一步导致错误“摘要已在进行中”。

通过在watch表达式中将$apply替换为$eval,范围变量将按预期更新。

因此,如果digest因为Angular中的一些其他更改而无论如何都要运行,那么$eval'ing就是您需要做的全部。

我在使用第三方脚本时遇到了同样的问题,例如CodeMirror和Krpano,甚至使用这里提到的安全应用方法也没有为我解决错误。

但是解决它的方法是使用$timeout服务(不要忘记先注入它)。

因此,类似于:

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

如果在您的代码中使用

这个

也许是因为它在工厂指令的控制器中,或者只是需要某种绑定,那么您可以这样做:

.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 functionthis.surprise();}));}
return new myClass();
}])

您也可以使用valAsync。它将在摘要完成后的某个时间运行!

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

从最近与Angular的人讨论这个话题:出于未来的原因,您不应该使用#0

当被要求以“正确”的方式去做时,答案是目前

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

我最近在编写角度服务来包装facebook、google和twitter API时遇到了这个问题,这些API在不同程度上提交了回调。

这是服务中的一个示例。(为了简洁起见,服务的其余部分-设置变量、注入$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($超时时间调用$browser.defer哪个如果未设置延迟,则默认为0

有点不直观,但这是编写Angular的人的答案,所以对我来说已经足够了!

Yerofmoo在为我们创建一个可重用的$安全应用函数方面做得很好:

用法:

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

了解Angular文档调用检查$$phase反模式,我尝试让$timeout_.defer工作。

超时和延迟方法在dom中创建了一个像FOUT一样的未解析\{\{myVar}}内容的闪光。对我来说,这是不可接受的。它让我没有太多教条地告诉我某些东西是黑客攻击,并且没有合适的替代方案。

每次都有效的唯一方法是:

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

我不明白这种方法的危险,也不明白为什么它被评论和角度团队中的人描述为黑客。命令似乎准确且易于阅读:

“做摘要,除非一个已经发生”

在CoffeeScript中,它甚至更漂亮:

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

这有什么问题?有没有一种不会产生FOUT的替代方案?$安全应用看起来不错,但也使用了$$phase检查方法。

我建议您使用自定义事件,而不是触发摘要循环。

我发现广播自定义事件和注册此事件的听众是触发您希望发生的操作的好解决方案,无论您是否处于摘要周期中。

通过创建自定义事件,您的代码也会更高效,因为您只触发订阅上述事件的侦听器,而不是触发绑定到范围的所有监视,就像您调用范围一样。

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

$scope.$broadcast('customEventName', optionalCustomEventArguments);

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

当您调用$apply时,问题就会出现,该调用有时在Angular代码之外异步运行(当应该使用$应用时),有时在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...

只有当您知道运行它的代码将始终在Angular代码之外运行时才调用$apply(例如,您对$应用的调用将发生在由Angular代码之外的代码调用的回调中)。

除非有人意识到使用$timeout而不是$apply的一些有影响力的缺点,否则我不明白为什么你不能总是使用$timeout(零延迟)而不是$apply,因为它会做大致相同的事情。

找到这个:https://coderwall.com/p/ngisma,其中Nathan Walker(靠近页面底部)建议在$rootScope中使用装饰器来创建func“安全应用”,代码:

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

这里的许多答案都包含了很好的建议,但也可能导致混乱。简单地使用$timeout没有最好也不是正确的解决方案。此外,如果您担心性能或可扩展性,请务必阅读。

你应该知道的事

  • $$phase对框架来说是私有的,这是有充分理由的。

  • $timeout(callback)将等到当前摘要周期(如果有)完成,然后执行回调,然后在最后运行一个完整的$apply

  • $timeout(callback, delay, false)也会做同样的事情(在执行回调之前有一个可选的延迟),但不会触发$apply(第三个参数),如果你没有修改你的Angular模型($scope),它会节省性能。

  • $scope.$apply(callback)调用$rootScope.$digest,这意味着它将重新消化应用程序的根范围及其所有子范围,即使您在独立的范围内。

  • $scope.$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 thatfunction is called synchronously from Angular or from anasynchronous code */}
// Processed by Angular, for instance called by a ng-click directive$scope.applyModelSynchronously = function() {// No need to digesteditModel();}
// Any kind of asynchronous code, for instance a server requestcallServer(function() {/* That code is not watched nor digested by Angular, thus wecan safely $apply it */$scope.$apply(editModel);});

如果你知道自己在做什么,并且在一个大型Angular应用程序的一部分上处理一个孤立的小指令,那么你可能更喜欢$digest而不是$应用来节省性能。

自Angularjs 1.2以来的更新

一个新的、强大的方法已经添加到任何$scope:$evalAsync中。基本上,如果发生回调,它将在当前摘要周期内执行回调,否则新的摘要周期将开始执行回调。

这仍然不如$scope.$digest好,如果你真的知道你只需要同步超文本标记语言的一个孤立的部分(因为一个新的$apply将被触发,如果没有正在进行中),但这是最好的解决方案,当你执行一个函数您无法知道它是否会同步执行,例如在获取可能缓存的资源后:有时这将需要一个异步调用到服务器,否则资源将在本地同步获取。

在这些情况下,以及所有其他你有!$scope.$$phase的情况下,请务必使用$scope.$evalAsync( callback )

您应该根据上下文使用$valAsync或$timeout。

这是一个很好的解释链接:

安全$apply的最短形式是:

$timeout(angular.noop)

这将解决您的问题:

if(!$scope.$$phase) {//TODO}

使用$scope.$$phase || $scope.$apply();代替

这是我的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))elsecallback();}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']);};

类似于上面的答案,但这对我来说是忠实的…在服务中添加:

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

我一直在使用这种方法,它似乎工作得很好。这只是等待循环结束的时间,然后触发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);}}

您可以使用#0来防止错误。

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

首先,不要用这种方式解决问题

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

这没有意义,因为$阶段只是$digest循环的布尔标志,所以您的$应用()有时不会运行。记住这是一个糟糕的做法。

使用$timeout

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

如果你使用的是下划线或洛达什,你可以使用deer():

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

尝试使用

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

而不是

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

$应用异步安排稍后调用$应用。这可用于将需要在同一摘要中计算的多个表达式排队。

注意:在$digest中,只有当当前作用域是$rootScope时,$appyAsync()才会刷新。这意味着如果您在子作用域上调用$digest,它不会隐式刷新$appyAsync()队列。

例子:

  $scope.$applyAsync(function () {if (!authService.authenticated) {return;}
if (vm.file !== null) {loadService.setState(SignWizardStates.SIGN);} else {loadService.setState(SignWizardStates.UPLOAD_FILE);}});

参考文献:

1.范围。$ApplyAsync()与范围。AngularJS 1.3中的$valAsync()

  1. AngularJs文档

当我禁用调试器时,错误不再发生。在我的情况下,这是因为调试器停止了代码执行。

问题基本上是来的时候,我们请求角运行摘要循环,即使它在过程中,这是创建问题角理解。控制台中的后果异常。
1.在$timeout函数中调用范围.$应用()没有任何意义,因为它在内部做同样的事情。
2.代码使用vanilla JavaScript函数,因为它的本机不是角度角度定义的,即setTimeout
3.要做到这一点,您可以使用

if(!




当前工作状态作用域。$eva lA sync(function(){

});

        let $timeoutPromise = null;$timeout.cancel($timeoutPromise);$timeoutPromise = $timeout(() => {$scope.$digest();}, 0, false);

这是一个很好的解决方案避免此错误并避免$应用

如果基于外部事件调用,您可以将其与防抖(0)结合使用。上面是我们正在使用的“防抖”,以及完整的代码示例

.factory('debounce', ['$timeout',function ($timeout) {
return function (func, wait, apply) {// apply default is true for $timeoutif (apply !== false) {apply = true;}
var promise;return function () {var cntx = this,args = arguments;$timeout.cancel(promise);promise = $timeout(function () {return func.apply(cntx, args);}, wait, apply);return promise;};};}])

和代码本身来监听一些事件和只在你需要的$作用域上调用$digest

        let $timeoutPromise = null;let $update = debounce(function () {$timeout.cancel($timeoutPromise);$timeoutPromise = $timeout(() => {$scope.$digest();}, 0, false);}, 0, false);
let $unwatchModelChanges = $scope.$root.$on('updatePropertiesInspector', function () {$update();});

$scope.$on('$destroy', () => {$timeout.cancel($update);$timeout.cancel($timeoutPromise);$unwatchModelChanges();});