为什么要使用if(!$ scope。$$ phase)$ scope。$ apply()作为反模式?


92

有时我需要$scope.$apply在代码中使用它,有时它会引发“已消化的摘要”错误。所以我开始找到解决这个问题的方法,并找到了这个问题:AngularJS:防止在调用$ scope。$ apply()时发生错误$ digest。但是,您可以在评论中(和角度维基上)阅读:

如果(!$ scope。$$ phase)$ scope。$ apply()不执行,那意味着您的$ scope。$ apply()在调用堆栈中不够高。

所以现在我有两个问题:

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

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

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

那是路要走吗?更安全吗?所以这才是真正的问题:我该怎么办完全消除“已消化的消化”错误的可能性?

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


根据我的经验,您应该始终知道是scope从角度内部还是从角度外部进行操作。因此,根据此信息,您始终知道是否需要致电scope.$apply。而且,如果您对角/非角scope操作都使用相同的代码,那么您做错了,它应该始终分开...因此,基本上,如果遇到需要检查的情况scope.$$phase,您的代码就不会以正确的方式进行设计,并且总有一种“正确的方式”进行操作
doodeec 2014年

1
我只在非角度回调中使用它(!)这就是为什么我很困惑
Dominik Goltermann 2014年

2
如果它是非角度的,则不会引发digest already in progress错误
doodeec 2014年

1
我也这么想。关键是:它并不总是抛出错误。只是偶尔一次。我的怀疑是该申请与另一个摘要发生偶然性冲突。那可能吗?
Dominik Goltermann 2014年

我认为如果回调严格地是非角度的,那是不可能的
doodeec 2014年

Answers:


113

经过进一步的挖掘,我能够解决是否始终可以安全使用的问题$scope.$apply。简短的答案是肯定的。

长答案:

由于您的浏览器执行Javascript的方式,两个摘要调用不可能偶然碰撞。

我们编写的JavaScript代码并非一口气运行,而是轮流执行。这些转弯中的每一个从开始到结束都不会中断,并且当转弯运行时,我们的浏览器中没有其他任何事情。(摘自http://jimhoskins.com/2012/12/17/angularjs-and-apply.html

因此,仅在一种情况下会发生错误“已经消化”:在另一个$ app内部发出$ app时,例如:

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

如果我们在纯非angularjs回调中使用$ scope.apply,则不会出现这种情况,例如。所以下面的代码是100%防弹有没有必要做一个setTimeoutif (!$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正确的方式,则根本不需要它:例如,在纯js回调中setTimeout

阅读http://jimhoskins.com/2012/12/17/angularjs-and-apply.html以获得更详细的说明。


我在那里我创建一个服务用一个例子 $document.bind('keydown', function(e) { $rootScope.$apply(function() { // a passed through function from the controller gets executed here }); });我,真的不知道为什么我必须做出$适用于这里,因为我使用的是$ document.bind ..
贝蒂圣

因为$ document只是“浏览器的window.document对象的jQuery或jqLit​​e包装器”。并实现如下:function $DocumentProvider(){ this.$get = ['$window', function(window){ return jqLite(window.document); }]; }那里没有适用。
Dominik Goltermann 2014年

11
$timeout语义上意味着延迟后运行代码。这样做从功能上讲可能是安全的,但这是hack。当您不知道某个$digest循环正在进行中还是已经进入时,应该有一种安全的方式使用$ apply $apply
约翰·斯特里克勒

1
糟糕的另一个原因:它使用内部变量($$ phase),它们不是公共api的一部分,并且它们可能会在较新版本的angular中更改,从而破坏您的代码。但是,与同步事件触发有关的问题很有趣
Dominik Goltermann

4
较新的方法是使用$ scope。$ evalAsync()(如果可能)在当前摘要周期或下一个周期安全地执行。请参阅bennadel.com/blog/...
jaymjarri

16

现在绝对是一种反模式。即使您检查$$阶段,我也看到摘要爆炸了。您只是不应该访问由$$前缀表示的内部API 。

你应该用

 $scope.$evalAsync();

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


9

在任何情况下,如果摘要正在进行中,而您又推送另一个服务来进行摘要,则只会给出错误,即摘要已经在进行中。所以要解决这个问题,您有两种选择。您可以检查正在进行的任何其他摘要,例如轮询。

第一

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

如果上述条件成立,则可以应用$ scope。$ apply否则不可以,并且

第二个解决方案是使用$ timeout

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

在$ timeout完成执行之前,它不会让另一个摘要开始。


1
不赞成 这个问题专门问为什么不按照您在此处描述的方式去做,而不是去破解它。有关何时使用的信息,请参见@gaul的出色回答$scope.$apply();
PureSpider 2014年

虽然没有回答问题:$timeout关键是!它有效,后来我发现它也是推荐的。
Himel Nag Rana 2015年

我知道在两年后再添加评论已经为时已晚,但是在过多使用$ timeout时要小心,因为如果您没有良好的应用程序结构,这可能会
浪费

9

scope.$apply触发一个$digest周期,这是2路数据绑定的基础

一个$digest循环检查对象,即$watch附加的对象(准确地说是模型),$scope以评估其值是否已更改,以及是否检测到更改,因此将采取必要的步骤来更新视图。

现在,当您使用$scope.$apply它时,会遇到一个错误“已在进行中”,因此很明显$ digest正在运行,但是触发它的原因是什么?

ans->每次$http调用,所有ng单击,重复,显示,隐藏等都会触发一个$digest循环,最糟糕的部分是每个$ SCOPE。

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

如果$scope每个属性中都有4个属性,则页面上总共有16个$ scope属性。

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

答案->$scope.$digest会触发$digeston子级和相同作用域,因此它将仅检查4个属性。因此,如果您确定D的更改不会影响A,B,C,请使用$scope.$digest not $scope.$apply

因此,$digest即使用户未触发任何事件,仅ng-click或ng-show / hide都可能触发超过100个属性的循环!


2
是的,很不幸,我在项目后期才意识到这一点。如果我从一开始就知道,就不会使用Angular。所有标准指令都将触发$ scope。$ apply,后者依次调用$ rootScope。$ digest,它将对所有范围执行脏检查。如果您问我,设计决定很差。我应该控制应该对哪些范围进行脏检查,因为我知道如何将数据链接到这些范围!
MoonStom 2014年

0

$timeout,这是推荐的方法。

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

我也尝试了以下方法,并且有效。对我来说,不同之处在于$ timeout更清晰。

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

将套接字代码包装为$ apply更加干净(就像Angular在AJAX代码上即$http)。否则,您必须在各处重复此代码。
timruffles

绝对不建议这样做。另外,如果$ scope具有$$ phase,则有时会出错。相反,您应该使用$ scope。$ evalAsync();。
FlavorScape

没有必要的$scope.$apply,如果你正在使用setTimeout$timeout
注:Kunal

-1

我发现了很酷的解决方案:

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