为什么使用 if (! $scope. $$stage) $scope. $application()是一种反模式?

有时我需要在我的代码中使用 $scope.$apply,有时它会抛出一个“摘要已经在进行中”的错误。所以我开始寻找一种方法来解决这个问题: AngularJS: 在调用 $scope 时防止已经在进行的错误 $摘要。然而,在评论(以及角度分明的维基)中,你可以看到:

不要执行 if (! $scope. $$stage) $scope. $application () ,这意味着您的 $scope. $application ()在调用堆栈中位置不够高。

现在我有两个问题:

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

另一个防止“正在消化”错误的“解决方案”似乎是使用 $timeout:

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

是这样吗?这样安全吗?因此,这里是真正的问题: 我如何才能消除 完全相信的可能性“摘要已经在进行中”错误?

PS: 我只使用 $scope。$application 在非同步的非 angularjs 回调中。(据我所知,在这些情况下必须使用 $scope。$application,如果您希望应用您的更改)

54753 次浏览

scope.$apply triggers a $digest cycle which is fundamental to 2-way data binding

A $digest cycle checks for objects i.e. models(to be precise $watch) attached to $scope to assess if their values have changed and if it detects a change then it takes necessary steps to update the view.

Now when you use $scope.$apply you face an error "Already in progress" so it is quite obvious that a $digest is running but what triggered it?

ans--> every $http calls, all ng-click, repeat, show, hide etc trigger a $digest cycle AND THE WORST PART IT RUNS OF EVERY $SCOPE.

ie say your page has 4 controllers or directives A,B,C,D

If you have 4 $scope properties in each of them then you have a total of 16 $scope properties on your page.

If you trigger $scope.$apply in controller D then a $digest cycle will check for all 16 values!!! plus all the $rootScope properties.

Answer-->but $scope.$digest triggers a $digest on child and same scope so it will check only 4 properties. So if you are sure that changes in D will not affect A, B, C then use $scope.$digest not $scope.$apply.

So a mere ng-click or ng-show/hide might be triggering a $digest cycle on over 100+ properties even when the user has not fired any event!

In any case when your digest in progress and you push another service to digest, it simply gives an error i.e. digest already in progress. so to cure this you have two option. you can check for anyother digest in progress like polling.

First one

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

if the above condition is true, then you can apply your $scope.$apply otherwies not and

second solution is use $timeout

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

it will not let the other digest to start untill $timeout complete it's execution.

After some more digging i was able to solve the question whether it's always safe to use $scope.$apply. The short answer is yes.

Long answer:

Due to how your browser executes Javascript, it is not possible that two digest calls collide by chance.

The JavaScript code we write doesn’t all run in one go, instead it executes in turns. Each of these turns runs uninterupted from start to finish, and when a turn is running, nothing else happens in our browser. (from http://jimhoskins.com/2012/12/17/angularjs-and-apply.html)

Hence the error "digest already in progress" can only occur in one situation: When an $apply is issued inside another $apply, e.g.:

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

This situation can not arise if we use $scope.apply in a pure non-angularjs callback, like for example the callback of setTimeout. So the following code is 100% bulletproof and there is no need to do a if (!$scope.$$phase) $scope.$apply()

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

even this one is safe:

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

What is NOT safe (because $timeout - like all angularjs helpers - already calls $scope.$apply for you):

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

This also explains why the usage of if (!$scope.$$phase) $scope.$apply() is an anti-pattern. You simply don't need it if you use $scope.$apply in the correct way: In a pure js callback like setTimeout for example.

Read http://jimhoskins.com/2012/12/17/angularjs-and-apply.html for the more detailed explanation.

Use $timeout, it is the way recommended.

My scenario is that I need to change items on the page based on the data I received from a WebSocket. And since it is outside of Angular, without the $timeout, the only model will be changed but not the view. Because Angular doesn't know that piece of data has been changed. $timeout is basically telling Angular to make the change in the next round of $digest.

I tried the following as well and it works. The difference to me is that $timeout is clearer.

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

It is most definitely an anti-pattern now. I've seen a digest blow up even if you check for the $$phase. You're just not supposed to access the internal API denoted by $$ prefixes.

You should use

 $scope.$evalAsync();

as this is the preferred method in Angular ^1.4 and is specifically exposed as an API for the application layer.

I found very cool solution:

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

inject that where you need:

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