为什么使用if(!$ scope。$$阶段)$ scope。$ apply()是一个反模式?

有时我需要使用$scope.$apply在我的代码中应用,有时会抛出一个“摘要已经在进行中”的错误。 所以我开始find一个解决这个问题的方法,并发现这个问题: AngularJS:当调用$ scope。$ apply()时,防止错误$ digest正在进行 。 然而在评论(和在维基上)你可以阅读:

不要做($ $ scope。$$阶段)$ scope。$ apply(),这意味着你的$ scope。$ apply()在调用栈中不够高。

所以现在我有两个问题:

  1. 为什么这是一个反模式?
  2. 我如何安全地使用$ scope。$ apply?

另一个“解决scheme”,以防止“摘要已经在进行中”的错误似乎是使用$超时:

 $timeout(function() { //... }); 

这是要走的路吗? 更安全吗? 所以这里是一个真正的问题:我怎样才能完全消除“摘要已经在进行”错误的可能性?

PS:我只使用$ scope。$ apply在非同步的非angularjscallback中。 (据我所知,这些情况下,你必须使用$ scope。$ apply,如果你想要更改被应用)

经过一些挖掘,我能够解决这个问题是否总是安全的使用$scope.$apply 。 简短的答案是肯定的。

很长的回答:

由于您的浏览器如何执行Javascript,两个摘要调用不可能偶然发生冲突。

我们编写的JavaScript代码并不是全部运行,而是轮stream执行。 每一轮都从头到尾不间断地运行,当一个回合正在运行时,我们的浏览器中没有任何事情发生。 (来自http://jimhoskins.com/2012/12/17/angularjs-and-apply.html

因此,“摘要已经在进行中”的错误只能发生在一种情况下:当$ apply在另一个$ apply内发出时,例如:

 $scope.apply(function() { // some code... $scope.apply(function() { ... }); }); 

如果我们在一个纯的非angularjscallback中使用$ scope.apply,就不会出现这种情况,比如setTimeout的callback。 所以下面的代码是100%防弹的,并且不需要做一个if (!$scope.$$phase) $scope.$apply()

 setTimeout(function () { $scope.$apply(function () { $scope.message = "Timeout called!"; }); }, 2000); 

即使这个是安全的:

 $scope.$apply(function () { setTimeout(function () { $scope.$apply(function () { $scope.message = "Timeout called!"; }); }, 2000); }); 

什么是不安全的(因为$超时 – 就像所有的angularjs助手 – 已经调用$scope.$apply你):

 $timeout(function () { $scope.$apply(function () { $scope.message = "Timeout called!"; }); }, 2000); 

这也解释了为什么if (!$scope.$$phase) $scope.$apply()是一个反模式。 如果你使用$scope.$apply你就不需要它了$scope.$apply以正确的方式应用:例如在一个纯的jscallback中,比如setTimeout

阅读http://jimhoskins.com/2012/12/17/angularjs-and-apply.html获取更详细的解释。;

在任何情况下,当你的摘要正在进行,你推另一个服务消化,它只是给出了一个错误,即摘要已经在进行中。 所以要治好这个,你有两个select。 您可以检查正在进行的其他摘要,如轮询。

第一

 if ($scope.$root.$$phase != '$apply' && $scope.$root.$$phase != '$digest') { $scope.$apply(); } 

如果上述条件是真的,那么你可以申请你的$ scope。$ apply otherwies not和

第二个解决scheme是使用$超时

 $timeout(function() { //... }) 

它不会让其他摘要启动直到$ timeout完成它的执行。

现在肯定是反模式。 即使你检查$$阶段,我也看到了一个消化爆炸。 你只是不应该访问由$$前缀表示的内部API。

你应该使用

  $scope.$evalAsync(); 

因为这是Angular ^ 1.4中的首选方法,并且专门作为应用程序层的API公开。

scope.$apply触发一个$digest循环,它是双向数据绑定的基础

$digest循环检查对象,即附加到$scope模型(准确地说$watch )以评估它们的值是否已经改变,如果它检测到改变,那么它将采取必要的步骤来更新视图。

现在,当你使用$scope.$apply你会遇到一个错误“已经在进行中”, 所以很明显,$摘要正在运行,但是什么触发了它?

每一个$http呼叫,所有ng点击,重复,显示,隐藏等触发一个$digest周期和最坏的部分运行每个$范围。

即说你的页面有4个控制器或指令A,B,C,D

如果你的每个属性有4个$scope属性,那么你的页面总共有16个$ scope属性。

如果你触发$scope.$apply在控制器D中$scope.$apply ,那么$digest循环将检查所有的16个值! 加上所有的$ rootScope属性。

回答 – >$scope.$digest触发一个$digest的子和相同的范围,所以它将只检查4个属性。 所以如果你确定D中的变化不会影响A,B,C,那么使用$scope.$diges t not $scope.$apply

因此,即使用户没有发起任何事件,单纯的ng-click或ng-show / hide可能会触发超过100多个属性的$digest循环!

使用$timeout ,这是build议的方式。

我的情况是,我需要根据从WebSocket收到的数据更改页面上的项目。 而且由于它在Angular之外,没有$超时,唯一的模型将被改变,但不是视图。 因为Angular不知道那块数据已经改变了。 $timeout基本上是告诉Angular在下一轮$摘要中进行更改。

我也尝试了以下,它的工作原理。 与我不同的是$超时更清晰。

 setTimeout(function(){ $scope.$apply(function(){ // changes }); },0) 

我发现非常酷的解决scheme:

 .factory('safeApply', [function($rootScope) { return function($scope, fn) { var phase = $scope.$root.$$phase; if (phase == '$apply' || phase == '$digest') { if (fn) { $scope.$eval(fn); } } else { if (fn) { $scope.$apply(fn); } else { $scope.$apply(); } } } }]) 

在需要的地方注入:

 .controller('MyCtrl', ['$scope', 'safeApply', function($scope, safeApply) { safeApply($scope); // no function passed in safeApply($scope, function() { // passing a function in }); } ])