AngularJS:调用$ scope。$ apply()时防止错误$ digest已经在进行中


838

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

我唯一知道的方法是$apply()从控制器和指令的范围进行调用。问题是它不断向显示以下内容的控制台抛出错误:

错误:$ digest已经在进行中

有谁知道如何避免这种错误或以不同的方式实现相同的目的?


34
确实令人沮丧的是,我们需要越来越多地使用$ apply。
OZ_

即使我在回调中调用$ apply,我也收到此错误。我正在使用第三方库来访问其服务器上的数据,所以我无法利用$ http,也不想这样做,因为我必须重写其库才能使用$ http。
Trevor

45
使用$timeout()
OnurYıldırım2014年

6
使用$ timeout(fn)+ 1,可以解决问题!$ scope。$$ phase不是最佳解决方案。
Huei Tan

1
仅包装代码/调用范围。$ 超时(不是$ timeout)内应用AJAX函数(不是$ http)和事件(不是ng-*)。确保,如果你是在一个函数中调用它(即通过超时/ AJAX /事件调用),它没有正在对负荷运行开始。
帕特里克

Answers:


660

不要使用此模式 -最终将导致更多错误,无法解决。即使您认为它可以修复某些问题,也没有。

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

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

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

就@ dnc253而言,如果您发现自己打电话$digest$apply经常打电话,则可能是做错了。我通常发现,由于Ang事件无法触发DOM事件而需要更新作用域的状态时,我通常需要进行摘要。例如,当一个Twitter引导模式被隐藏时。有时DOM事件在a $digest进行时触发,有时不触发。这就是为什么我使用这张支票。

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


来自评论:@anddoutoi

angular.js反模式

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

230
在我看来,$ digest / $ apply应该默认执行此操作
Roy Truelove

21
请注意,在某些情况下,我必须检查当前范围和根范围。我已经在根目录上获得了$$ phase的值,但在我的范围上却没有。认为它与指令的隔离范围有关,但是..
Roy Truelove

106
“停止执行if (!$scope.$$phase) $scope.$apply()”,github.com
angular/

34
@anddoutoi:同意;您的链接非常清楚,这不是解决方案;但是,我不确定“您在调用堆栈中不够高”的含义。你知道这是什么意思吗?
Trevor

13
@threed:请参阅aaronfrost的答案。正确的方法是使用defer在下一个周期中触发摘要。否则,事件将丢失并且根本不会更新作用域。
Marek 2013年

663

最近与Angular员工就此主题进行了讨论:出于面向未来的原因,不应使用$$phase

当按“正确”的方式操作时,当前答案是

$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的delay参数是可选的,如果未设置则默认为0($ timeout调用$ browser.defer如果未设置delay则默认为0)。

有点非直觉,但这就是编写Angular的人的答案,所以对我来说足够好了!


5
我在指令中遇到了很多次。正在为编剧编写一个,事实证明它可以正常工作。我在与Brad Green的一次聚会中,他说Angular 2.0将会非常强大,并且没有使用JS的本机观察功能并为缺少该功能的浏览器使用polyfill的摘要周期。届时,我们将不再需要这样做。:)
Michael J. Calkins

昨天我看到了一个问题,其中在$ timeout中调用selectize.refreshItems()导致了可怕的递归摘要错误。任何想法怎么可能?
iwein

3
如果使用$timeout而不是本机setTimeout,为什么不使用$window本机window
LeeGee

2
@LeeGee:$timeout在这种情况下,使用的目的是$timeout确保正确更新角度范围。如果没有$ digest,则将导致运行新的$ digest。
敬畏

2
@webicy没什么。当传递给$ timeout的函数主体运行时,promise已经解决!绝对没有理由cancel。来自文档:“因此,诺言将被拒绝解决。” 您无法解决已解决的承诺。您的取消不会引起任何错误,但也不会做任何积极的事情。
daemonexmachina

324

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

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

或者如果您有lodash:

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

我们尝试了几种解决方法,但我们讨厌将$ rootScope注入所有控制器,指令甚至某些工厂。因此,到目前为止,$ timeout和_.defer是我们最喜欢的。这些方法成功地告诉angular等待下一个动画循环,这将确保当前scope。$ apply已经结束。


2
这相当于使用$ timeout(...)吗?我在几种情况下使用$ timeout来推迟到下一个事件周期,并且似乎工作得很好-任何人都知道是否有理由不使用$ timeout吗?
特雷弗2013年

9
仅当您已经在使用时,才应使用此功能underscore.js。仅使用其defer功能就不值得导入整个下划线库。我更喜欢该$timeout解决方案,因为每个人都已经可以$timeout通过angular 访问,而无需依赖其他库。
Tennisgent 2014年

10
是的,但是如果您不使用下划线或破折号,则需要重新评估您的操作。这两个库改变了代码的外观。
2014年

2
我们将lodash作为Restangular的依赖项(我们将尽快消除Restangular以便​​使用ng-route)。我认为这是一个很好的答案,但是假设人们要使用下划线/破折号并不是很好。不管怎么说,那些库都很好...如果您充分利用它们...这些天,我使用ES5方法,消除了我以前使用下划线的98%的原因。
布莱德格林(BradGreens)

2
您说对了@SgtPooki。我修改了答案,以包括也可以使用$ timeout的选项。$ timeout和_.defer都将等到下一个动画循环,这将确保当前范围。$ apply已结束。感谢您保持诚实,并让我在此处更新答案。
2014年

267

这里的许多答案都包含很好的建议,但也可能导致混乱。只需用$timeout不是最好的,也不是正确的解决方案。另外,如果您对性能或可伸缩性感到担心,请务必阅读。

你应该知道的事情

  • $$phase 该框架是私有的,并且有充分的理由。

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

  • $timeout(callback, delay, false)将执行相同的操作(在执行回调之前有一个可选的延迟),但不会触发$apply(第三个参数),如果您不修改Angular模型($ 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);
});

而且,如果您知道自己在做什么并且在大型Angular应用程序的一部分中使用一个孤立的小指令,则可以选择$ digest而不是$ apply来节省性能。

自Angularjs 1.2起更新

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

$scope.$digest如果您真的只知道同步HTML的孤立部分(因为$apply如果没有正在进行的情况下会触发新的同步),那仍然不如您想的那样,但这是执行函数时的最佳解决方案。这你可以不知道它是否会同步或不执行获取潜在缓存的资源之后,例如:有时这需要一个异步调用到服务器,否则,资源将在本地获取同步。

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


4
$timeout被批评通过。您能给出更多避免的理由$timeout吗?
mlhDev

88

方便的小帮手方法,可保持此过程干燥:

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

6
您的safeApply帮助我更了解发生了什么。感谢您发布。
詹森·

4
我将要做同样的事情,但这不是意味着$ digest有可能看不到我们在fn()中所做的更改吗?假定作用域为佳,延迟功能会更好吗?$$ phase ==='$ digest'吗?
Spencer Alger 2013年

我同意,有时$ apply()用来触发摘要,仅通过本身调用fn ...不会导致问题吗?
CMCDragonkai 2013年

1
我觉得 scope.$apply(fn);应该是scope.$apply(fn());因为fn()将执行函数而不是fn。请帮助我到哪里错了
madhu131313

1
@ZenOut对$ apply的调用支持许多不同种类的参数,包括函数。如果传递了函数,它将评估该函数。
boxmein 2016年

33

我在使用第三方脚本(例如CodeMirror和Krpano)时遇到了同样的问题,即使使用此处提到的safeApply方法也无法为我解决错误。

但是解决该问题的方法是使用$ 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 function
          this.surprise();
       }));
    }

    return new myClass();

  }]
)

32

参见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代码之外的代码调用的回调内发生)。

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


谢谢,这对于我不打电话给$apply自己但仍然出现错误的情况很有用。
ariscris 2014年

5
主要区别是$apply同步(执行其回调,然后执行$ apply之后的代码),$timeout而不同步:执行超时后的当前代码,然后一个新的堆栈以其回调开始,就像您正在使用一样setTimeout。如果您对同一模型进行两次更新,则可能会导致图形故障:$timeout将在刷新视图之前等待视图刷新。
floribon 2014年

的确,谢谢。由于某些$ watch活动,我有一个方法被调用,并且在我的外部过滤器执行完之前试图更新UI。将其放入$ timeout函数对我来说很有效。
djmarquette 2014年

28

当您收到此错误时,基本上意味着它已经在更新视图。您确实不需要$apply()在控制器内调用。如果您的视图未按预期更新,然后在调用时收到此错误$apply(),则很可能表示您没有正确更新模型。如果您发布一些细节,我们可以找出核心问题。


呵呵,我花了整整一天的时间才发现AngularJS不能“神奇地”观察绑定,有时我应该用$ apply()来推动他。
OZ_

到底意味着you're not updating the the model correctly什么?$scope.err_message = 'err message';是不正确的更新?
OZ_

2
唯一需要调用的时间$apply()是在更新angular的“外部”模型时(例如,从jQuery插件获取)。很容易陷入视图不正确的陷阱中,因此您$apply()到处都丢了一堆s,最后导致在OP中看到错误。当我说的时候,you're not updating the the model correctly我只是说所有业务逻辑都没有正确填充范围内的任何内容,这导致视图看起来不符合预期。
dnc253

@ dnc253我同意,我写下了答案。知道了我现在所知道的,我将使用$ timeout(function(){...}); 它的作用与_.defer相同。它们都遵循下一个动画循环。
2014年

14

保险柜的最短形式$apply是:

$timeout(angular.noop)

11

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

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

10

首先,不要这样解决

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

如果使用下划线或破折号,则可以使用defer():

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



5

尝试使用

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

代替

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

$ applyAsync安排$ apply的调用在以后发生。这可用于排队多个需要在同一摘要中求值的表达式。

注意:在$ digest中,仅当当前范围为$ rootScope时,才会刷新$ applyAsync()。这意味着,如果在子作用域上调用$ digest,则不会隐式刷新$ applyAsync()队列。

范例:

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

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

参考文献:

1. AngularJS 1.3中的Scope。$ applyAsync()与Scope。$ evalAsync()

  1. AngularJs文档

4

我建议您使用自定义事件,而不要触发摘要周期。

我发现,广播自定义事件并为此事件注册侦听器是触发您希望发生的操作的好方法,无论您是否处于摘要循环中。

通过创建自定义事件,您还可以提高代码效率,因为您仅触发订阅该事件的侦听器,而不像调用scope。$ apply那样触发绑定到范围的所有监视。

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


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

3

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

2

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

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

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

就我而言,ng-click更改范围内的变量,对该变量的$ watch更改必须为的其他变量$applied。最后一步导致错误“消化已在进行中”。

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

因此,似乎由于Angular中的某些其他更改而使摘要始终无法运行时,$eval只需执行ing操作。



1

理解的是,角文件调用检查$$phase反模式,我就先$timeout_.defer工作。

超时和延迟方法{{myVar}}在dom中像FOUT一样产生了无法解析的内容。对我来说,这是不可接受的。这让我没有太多的教条式地告诉我某事是hack,并且没有合适的替代方法。

每次唯一起作用的是:

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

我不了解这种方法的危险性,也无法理解为什么人们在评论和有角度的团队中将其描述为骇客行为。该命令似乎精确且易于阅读:

“进行摘要,除非已经发生”

在CoffeeScript中,它甚至更漂亮:

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

这是什么问题?是否有不会创建FOUT的替代方案?$ safeApply看起来不错,但也使用$$phase检查方法。


1
我希望看到对此问题的明智答复!
本·惠勒

这是一个骇客,因为这意味着您错过了上下文或在这一点上不了解代码:要么您处于角度摘要循环之内,并且您不需要它,要么异步地脱离了它,然后就需要了它。如果您在代码的那一点上不知道这一点,那么您就没有责任对其进行摘要
floribon 2015年

1

这是我的效用服务:

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

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

当我禁用调试器时,该错误不再发生。就我而言,这是因为调试器停止了代码执行。


0

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

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


0

当我们要求有角度地运行摘要循环时,这个问题基本上就来了,尽管它的过程正在引起人们对角度的理解。控制台中的结果异常。
1.在$ timeout函数中调用scope。$ apply()没有任何意义,因为在内部它是相同的。
2.该代码具有香草JavaScript函数,因为它的本机不是按角度定义的,即setTimeout。3
.为此,您可以使用

if(!scope。$$ phase){
scope。$ evalAsync(function(){

}); }


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

这是避免此错误并避免$ apply的好方法

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

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

        return function (func, wait, apply) {
            // apply default is true for $timeout
            if (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;
            };
        };
    }
])

和代码本身以侦听某些事件并仅在所需的$ scope上调用$ 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();
        });

-3

发现了这一点:https : //coderwall.com/p/ngisma,其中Nathan Walker(在页面底部)建议在$ 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;
      }
    ]);
  }
]);

-7

这将解决您的问题:

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

如果(!$ scope。$$ phase)$ scope。$ apply()不这样做,则意味着您的$ scope。$ apply()在调用堆栈中不够高。
MGot90
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.