Angularjs 为什么使用if(!$scope.$$phase)$scope.$apply()是反模式?

Angularjs 为什么使用if(!$scope.$$phase)$scope.$apply()是反模式?,angularjs,angularjs-scope,Angularjs,Angularjs Scope,有时我需要在代码中使用$scope.$apply,有时它会抛出“摘要已在进行中”错误。所以我开始想办法解决这个问题,发现了这个问题:。但是,在评论中(以及在angular wiki上),您可以阅读: 如果(!$scope.$$phase)$scope.$apply(),则不要这样做,这意味着您的$scope.$apply()在调用堆栈中的位置不够高 现在我有两个问题: 为什么这是一种反模式 如何安全地使用$scope.$apply 防止“摘要已在进行”错误的另一个“解决方案”似乎是使用$time

有时我需要在代码中使用
$scope.$apply
,有时它会抛出“摘要已在进行中”错误。所以我开始想办法解决这个问题,发现了这个问题:。但是,在评论中(以及在angular wiki上),您可以阅读:

如果(!$scope.$$phase)$scope.$apply(),则不要这样做,这意味着您的$scope.$apply()在调用堆栈中的位置不够高

现在我有两个问题:

  • 为什么这是一种反模式
  • 如何安全地使用$scope.$apply
  • 防止“摘要已在进行”错误的另一个“解决方案”似乎是使用$timeout:

    $timeout(function() {
      //...
    });
    
    这是路吗?安全吗?因此,真正的问题是:我如何完全消除“摘要已在进行”错误的可能性


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

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

    $digest
    循环检查对象,即连接到
    $scope
    的模型(准确地说,
    $watch
    ),以评估其值是否已更改,如果检测到更改,则采取必要步骤更新视图

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

    ans-->每调用一次
    $http
    ,所有ng单击、重复、显示、隐藏等都会触发一个
    $digest
    循环,并在每个$SCOPE中运行最差的部分

    比如说你的页面有4个控制器或指令A、B、C、D

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

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

    回答-->但
    $scope.$digest
    在子级和同一作用域上触发
    $digest
    ,因此它将只检查4个属性。因此,如果您确定D中的更改不会影响A、B、C,那么请使用
    $scope.$diges
    t not
    $scope.$apply


    因此,即使用户未触发任何事件,只要点击ng或显示/隐藏ng,也可能会在100多个属性上触发
    $digest
    循环

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

    第一个

    if ($scope.$root.$$phase != '$apply' && $scope.$root.$$phase != '$digest') {
        $scope.$apply();
    }
    
    如果上述条件为真,则可以应用$scope。$apply otherwies不是,并且

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

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

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

    在进一步挖掘之后,我能够解决使用
    $scope是否总是安全的问题。$apply
    。简而言之,答案是肯定的

    长答覆:

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

    我们编写的JavaScript代码不是一次性运行的,而是轮流执行。每个回合从开始到结束都不间断地运行,当一个回合运行时,浏览器中不会发生其他任何事情。(来自)

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

    $scope.apply(function() {
      // some code...
      $scope.apply(function() { ... });
    });
    
    如果
    我们使用$scope.apply在纯非angularjs回调中,例如,
    setTimeout
    的回调,则不会出现这种情况。因此,下面的代码是100%防弹的,无需执行
    if(!$scope.$$phase)$scope.$apply()

    即使这一个也是安全的:

    $scope.$apply(function () {
        setTimeout(function () {
            $scope.$apply(function () {
                $scope.message = "Timeout called!";
            });
        }, 2000);
    });
    
    什么是不安全的(因为$timeout-像所有angularjs助手一样-已经为您调用了
    $scope.$apply
    ):

    这也解释了为什么使用
    if(!$scope.$$phase)$scope.$apply()
    是一种反模式。如果以正确的方式使用
    $scope.$apply
    ,您根本不需要它:例如,在纯js回调中,如
    setTimeout


    阅读更多详细说明。

    使用
    $timeout
    ,这是推荐的方式

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

    我也尝试了以下方法,效果很好。对我来说,区别在于$timeout更清晰

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

    现在,这无疑是一种反模式。我已经看到一个摘要爆炸,即使你检查$$阶段。您不应该访问由
    $$
    前缀表示的内部API

    你应该使用

     $scope.$evalAsync();
    

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

    我找到了非常酷的解决方案:

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

    根据我的经验,您应该知道,如果您是从角度内还是角度外操作
    范围
    。因此,根据这一点,您总是知道是否需要调用
    scope.$apply
    。如果你对角度/非角度
    范围
    操作使用相同的代码,那么你做得不对,它应该总是分开。。。因此,基本上,如果您遇到需要检查
    范围。$$phase
    的情况,您的代码不是在co中设计的
    .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
            });
        }
    ])