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反模式
- 不要这样做
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函数:
用法:
//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将改变其他必须被$applied
variables。 最后一步导致错误“摘要已在进行中”。
通过在watchexpression式中用$eval
replace$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 }