AngularJS:调用$ scope时,防止错误$ digest正在进行中$ apply()

我发现我需要手动更新我的页面到我的范围越来越多,因为build立一个angular度的应用程序。

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

错误:$摘要已在进行中

有谁知道如何避免这个错误,或以不同的方式实现相同的事情?

不要使用这种模式 – 这会导致比解决更多的错误。 即使你认为它确定了一些东西,但事实并非如此。

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

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

$scope.$$phase将返回"$digest""$apply"如果正在进行$digest$apply 。 我相信这些国家之间的区别在于, $digest将处理当前范围内的手表及其子项, $apply将处理所有范围内的观察者。

至于@ dnc253的观点,如果你发现自己调用$digest或者$apply频繁$apply ,你可能会做错了。 当我需要更新范围的状态时,我通常会发现我需要消化,因为在Angular范围之外发生了DOM事件。 例如,当一个twitter bootstrap模式变得隐藏。 有时,当事件正在进行时,DOM事件触发,有时不会。 这就是为什么我使用这个检查。

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


来自评论:@anddoutoi

angular.js反模式

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

从最近与Angular人讨论这个话题: 为了未来的原因,你不应该使用$$phase

当按“正确”的方式来做到这一点,答案是目前

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

我最近在编写angular度服务来包装facebook,google和twitter API时遇到了这个问题,这些API在不同程度上都有callback。

以下是服务中的一个示例。 (为了简洁起见,剩下的服务 – 设置variables,注入$ 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 calls $ browser.defer 如果没有设置延迟,则默认为0 )

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

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

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

或者如果你有下划线:

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

我们尝试了几种解决方法,并且我们讨厌将$ rootScope注入到所有的控制器,指令甚至一些工厂中。 所以,$超时和_.defer是我们最喜欢的。 这些方法成功地告诉angular度等待,直到下一个animation循环,这将保证当前范围$ apply已经结束。

这里的许多答案都包含很好的意见,但也可能导致混淆。 简单地使用$timeout 不是最好的也不是正确的解决scheme。 另外,如果您担心性能或可伸缩性,请务必阅读。

你应该知道的事情

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

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

  • $timeout(callback, delay, false)将会执行相同的操作(在执行callback前有一个可选的延迟),但是如果你没有修改你的Angular模型, $apply (第三个参数) )。

  • $scope.$apply(callback)调用$rootScope.$digest ,这意味着即使您在一个独立的范围内,它也会重新标记应用程序及其所有子级的根作用域。

  • $scope.$digest()只是简单地将它的模型同步到视图中,但是不会消化它的父类作用域,这样可以节省大量的工作, 。 $摘要不需要callback:你执行代码,然后摘要。

  • $scope.$evalAsync(callback)已经和angularjs 1.2一起引入了,并且可能解决你的大部分问题。 请参考最后一段了解更多信息。

  • 如果你得到$digest already in progress error ,那么你的架构是错误的:或者你不需要重新定义你的范围,或者你不应该负责 (见下文)。

如何构build你的代码

当你得到这个错误的时候,你正在试图消化你的示波器,因为在那个时候你不知道你的示波器的状态,所以你不负责处理它的消化。

 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以来更新

一个新的强大的方法已经被添加到任何$范围: $evalAsync 。 基本上,它将在当前摘要周期内执行它的callback(如果正在发生),否则新的摘要周期将开始执行callback。

这还不如$scope.$digest如果你真的知道你只需要同步你的HTML的一个孤立的部分(因为如果没有正在进行的新的$apply将被触发),但这是最好的解决scheme,当你正在执行一个函数, 你不知道它是否会被同步执行 ,例如在获取可能被caching的资源之后:有时这需要对服务器进行asynchronous调用,否则资源将在本地被同步提取。

在这些情况下和所有其他地方你有一个!$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的调用时,会出现这个问题,有时在Angular代码之外(当应用$ apply时)asynchronous运行,有时在Angular代码内部同步(这会导致$digest already in progress错误)。

例如,当您有一个从服务器asynchronous获取项目并将其caching的库时,可能会发生这种情况。 第一次请求一个项目时,它将被asynchronous检索,以防止代码执行。 但是,第二次,该项目已经在caching中,因此可以同步检索。

防止这个错误的方法是确保调用$apply的代码是asynchronous运行的。 这可以通过在$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就可以了(例如,你的$ apply调用会在你的Angular代码之外的代码调用的callback中发生)。

除非有人知道使用$timeout超过$apply带来一些不利影响,否则我不明白为什么你不能总是使用$timeout (而不是延迟)而不是$apply ,因为它会做大致相同的事情。

我有像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(); }] ) 

当你得到这个错误时,基本上意味着它已经在更新你的视图了。 你真的不需要在你的控制器中调用$apply() 。 如果你的视图没有像你期望的那样更新,那么在调用$apply()之后你会得到这个错误,这很可能意味着你没有正确地更新模型。 如果你张贴一些细节,我们可以找出核心问题。

安全$apply的最短forms是:

 $timeout(angular.noop) 

您也可以使用evalAsync。 摘要结束后会运行一段时间!

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

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

尝试这个:

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

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

这是一个很好的解释:

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

我build议你使用自定义事件而不是触发摘要循环。

我已经发现,广播自定义事件并为这个事件注册监听器是一个很好的解决scheme,可以触发你想要发生的一个动作,而不pipe你是否处于一个摘要循环中。

通过创build自定义事件,您的代码效率也会更高,因为您只触发订阅了该事件的侦听器,而不会像触发范围那样触发绑定到该范围的所有手表。$ apply。

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

yearofmoo做了一个伟大的工作,为我们创build一个可重用的$ 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会在一个范围内改变一个variables,并且该variables上的$ watch将改变其他必须被$appliedvariables。 最后一步导致错误“摘要已在进行中”。

通过在watchexpression式中用$evalreplace$apply ,范围variables按照预期进行更新。

因此,如果由于Angular中的其他更改,摘要将会运行起来,那么您只需要$eval就可以了。

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

了解Angular文档呼吁检查$$phase 反模式 ,我试图得到$timeout_.defer工作。

超时和延期方法在FOM中创build一个未分析的{{myVar}}内容。 对我来说这是不能接受的。 这让我毫不留情地被告知有些东西是黑客,没有一个合适的select。

唯一的作品每一次是:

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

我不明白这种方法的危险性,也不知道为什么它被评论和angular色团队中的人们所诟病。 该命令看起来精确,易于阅读:

“除非已经发生,否则请摘要”

在CoffeeScript中,它更漂亮:

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

这有什么问题? 有没有不会创buildFOUT的替代scheme? $ 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(<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); } } 

类似于上面的答案,但这已经忠实地为我服务…在服务中添加:

  //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其中内森沃克(页的底部附近)build议$ rootScope中的装饰器创buildfunc'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 }