提问



我发现自从以角度构建应用程序后,我需要越来越多地手动将页面更新到我的范围。


我知道这样做的唯一方法是从我的控制器和指令的范围调用$apply()。这样做的问题是它不断向控制台抛出一个错误:



  错误:$ digest已在进行中



有谁知道如何避免这个错误或实现相同的事情,但以不同的方式?

最佳参考



  不要使用这种模式 - 这最终会造成比它解决的更多错误。即使你认为它修复了某些东西,它也没有。



您可以通过选中$scope.$$phase来检查$digest是否已在进行中。


if(!$scope.$$phase) {
  //$digest or $apply
}


如果$digest$apply正在进行中,$scope.$$phase将返回"$digest""$apply"。我相信这些状态之间的区别在于$digest将处理当前范围的手表及其子,$apply将处理所有范围的观察者。


至于@ dnc253,如果你发现自己经常调用$digest$apply,你可能做错了。我通常发现当我需要更新范围的状态时我需要消化DOM事件在Angular范围之外触发​​的结果。例如,当twitter引导模式变为隐藏时。有时,当$digest正在进行时,DOM事件会触发,有时则不会。这就是我使用这张支票的原因。


如果有人知道,我很想知道一个更好的方法。





来自评论:
通过@anddoutoi


angular.js反模式[100]



  

      
  1. 不要if (!$scope.$$phase) $scope.$apply(),这意味着你的$scope.$apply()在调用堆栈中不够高。

  2.   


其它参考1


最近与Angular人讨论了这个话题:出于未来的原因,你不应该使用$$phase


当按下正确方式时,答案是当前的


$timeout(function() {
  // anything you want can go here and will safely be run on the next digest.
})


我最近遇到了这个问题,当写角度服务来包装facebook,google和twitter API时,这些API在不同程度上都有回调。


这是服务中的一个例子。(为了简洁起见,服务的其余部分 - 设置变量,注入$ 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调用$ browser.defer,如果没有设置延迟,则默认为0)[101] [102] [103]


有点不直观,但这是写Angular的人的答案,所以对我来说这已经足够了!

其它参考2


摘要周期是同步调用。它不会对浏览器的事件循环产生控制权,直到完成为止。有几种方法可以解决这个问题。解决这个问题最简单的方法是使用内置的$ timeout,第二种方法是使用下划线或lodash(你应该这样),请调用以下内容:


$timeout(function(){
    //any code in here will automatically have an apply run afterwards
});


或者如果你有下划线:


_.defer(function(){$scope.$apply();});


我们尝试了几种解决方法,我们讨厌将$ rootScope注入我们所有的控制器,指令甚至一些工厂。所以,$ timeout和_.defer到目前为止一直是我们的最爱。这些方法成功地告诉angular要等到下一个动画循环,这将保证当前范围。$ apply结束。

其它参考3


这里的许多答案包含很好的建议,但也可能导致混淆。简单地使用$timeout是不最佳解决方案。
此外,如果您担心性能或可扩展性,请务必阅读。


你应该知道的事情




  • $$phase对框架是私有的,并且有充分的理由。

  • $timeout(callback)将等到当前的摘要周期(如果有的话)完成,然后执行回调,然后在结束时运行$apply

  • $timeout(callback, delay, false)将执行相同的操作(在执行回调之前有一个可选的延迟),但如果你没有修改你的Angular模型,则不会触发$apply(第三个参数)来保存性能( $范围)。

  • $scope.$apply(callback)除其他外调用$rootScope.$digest,这意味着它将重新删除应用程序及其所有子项的根范围,即使您处于一个孤立的范围内。

  • $scope.$digest()将简单地将其模型同步到视图,但不会消化其父节点范围,这可以在使用隔离范围处理HTML的孤立部分时节省大量性能(主要来自指令) )。 $ digest不接受回调:你执行代码,然后消化。

  • $scope.$evalAsync(callback)已经引入angularjs 1.2,可能会解决你的大部分麻烦。请参阅最后一段以了解更多相关信息。

  • 如果你得到$digest already in progress error,那么你的架构是错误的:要么你不需要重新删除你的范围,要么你不应该负责那个(见下文) 。



如何构建代码



当你得到那个错误时,你正在尝试消化你的范围,因为它已经在进行中:因为你不知道你的范围的状态,你不负责处理它的消化。


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

以来的更新

任何$ scope都添加了一种新的强大方法:$evalAsync。基本上,如果一个正在发生,它将在当前摘要周期内执行其回调,否则新的摘要周期将开始执行回调。


如果你真的知道你只需要同步一个孤立的HTML部分(因为如果没有进行就会触发新的$apply),那仍然不如$scope.$digest那么好,但是这个当你执行一个你无法知道它同时执行或不执行的函数时是最好的解决方案,例如在获取可能缓存的资源之后:有时这需要对服务器进行异步调用,否则资源将在本地同步获取。


在这些情况下以及您!$scope.$$phase的所有其他情况下,请务必使用$scope.$evalAsync( callback )

其它参考4


方便的小助手方法来保持这个过程干燥:


function safeApply(scope, fn) {
    (scope.$$phase || scope.$root.$$phase) ? fn() : scope.$apply(fn);
}

其它参考5


见http://docs.angularjs.org/error/$rootScope:inprog [104]


当您调用$apply有时会在Angular代码之外异步运行(当应用$ apply时)并且有时在Angular代码内同步运行(导致$digest already in progress错误时)会出现问题。


例如,当您有一个从服务器异步获取项目并缓存它们的库时,可能会发生这种情况。第一次请求项时,将异步检索它,以免阻止代码执行。但是,第二次,该项目已经在缓存中,因此可以同步检索它。


防止此错误的方法是确保调用$apply的代码异步运行。这可以通过在$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...


只有在知道运行它的代码时才调用$apply将始终在Angular代码之外运行(例如,对$ apply的调用将发生在由Angular代码之外的代码调用的回调中)。


除非有人意识到$timeout超过$apply使用$apply的一些影响不利,否则我不明白为什么你不能总是使用$timeout(零延迟)而不是$apply]],因为它会做大致相同的事情。

其它参考6


我遇到了像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();

  }]
)

其它参考7


当你收到这个错误时,它基本上意味着它已经在更新你的视图的过程中。你真的不需要在你的控制器中调用$apply()。如果您的视图没有像您期望的那样更新,然后在调用$apply()后出现此错误,则很可能意味着您没有正确更新模型。如果你发布一些细节,我们可以找出核心问题。

其它参考8


最安全的$apply形式是:


$timeout(angular.noop)

其它参考9


您也可以使用evalAsync。摘要完成后它会运行一段时间!


scope.evalAsync(function(scope){
    //use the scope...
});

其它参考10


如果您使用这种方式,有时您仍会遇到错误(https://stackoverflow.com/a/12859093/801426).


尝试这个:


if(! $rootScope.$root.$$phase) {
...

其它参考11


您应该根据上下文使用$ evalAsync或$ timeout。


这是一个很好解释的链接:



  http://www.bennadel.com/blog/2605-scope-evalasync-vs-timeout-in-angularjs.htm[106]


其它参考12


首先,不要这样修复它


if ( ! $scope.$$phase) { 
  $scope.$apply(); 
}


它没有意义,因为$ phase只是$ digest循环的布尔标志,所以你的$ apply()有时不会运行。记住这是一个不好的做法。


相反,使用$timeout


    $timeout(function(){ 
  // Any code in here will automatically have an $scope.apply() run afterwards 
$scope.myvar = newValue; 
  // And it just works! 
});


如果您使用下划线或lodash,则可以使用defer():


_.defer(function(){ 
  $scope.$apply(); 
});

其它参考13


我建议你使用自定义事件而不是触发摘要周期。


我发现广播自定义事件和为此事件注册听众是一个很好的解决方案,无论您是否处于摘要周期,都可以触发您希望发生的操作。


通过创建自定义事件,您对代码的效率也更高,因为您只触发订阅所述事件的侦听器,而不是像调用范围那样触发绑定到范围的所有监视。$ apply。


$scope.$on('customEventName', function (optionalCustomEventArguments) {
   //TODO: Respond to event
});


$scope.$broadcast('customEventName', optionalCustomEventArguments);

其它参考14


yearofmoo为我们创建可重用的$ safeApply功能做得很好:



  https://github.com/yearofmoo/AngularJS-Scope.SafeApply[107]



用法:


//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);

其它参考15


我已经能够通过在我知道$digest函数将运行的地方调用$eval而不是$apply来解决这个问题。


根据文件,$apply基本上这样做:[108]


function $apply(expr) {
  try {
    return $eval(expr);
  } catch (e) {
    $exceptionHandler(e);
  } finally {
    $root.$digest();
  }
}


在我的例子中,ng-click更改范围内的变量,并且对该变量的$ watch更改必须为$applied的其他变量。最后一步导致错误摘要已在进行中。


通过在监视表达式中用$eval替换$apply,范围变量按预期更新。


因此,它出现如果由于Angular中的其他一些变化,如果摘要将继续运行,$eval就是你需要做的。

其它参考16


请改用$scope.$$phase || $scope.$apply();

其它参考17


理解Angular文档调用$$phase反模式,我试图让$timeout_.defer起作用。[109]


超时和延迟方法在dom中创建了未解析的{{myVar}}内容,就像FOUT一样。对我来说这是不可接受的。这让我没有多少被教条地告诉我某事是黑客攻击,而没有合适的选择。[110]


唯一有效的方法是:


if(scope.$$phase !== '$digest'){ scope.$digest() }


我不明白这种方法的危险性,或者为什么它在评论和角度团队中被人们描述为黑客。该命令看起来精确且易于阅读:



  做消化,除非已经发生了



在CoffeeScript中,它甚至更漂亮:


scope.$digest() unless scope.$$phase is '$digest'


这有什么问题?有没有一个替代品不会创造一个FOUT? $ safeApply看起来很好,但也使用$$phase检查方法。[111]

其它参考18


这是我的utils服务:


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

其它参考19


我一直在使用这种方法,它似乎工作得非常好。这只是等待循环完成的时间然后触发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);
  }
}

其它参考20


尝试使用


$scope.applyAsync(function() {
    // your code
});


代替


if(!$scope.$$phase) {
  //$digest or $apply
}


$ applyAsync安排$ apply的调用以后发生。这可用于排队需要在同一摘要中评估的多个表达式。


注意:在$ digest中,$ applyAsync()仅在当前作用域为$ rootScope时才会刷新。这意味着如果在子作用域上调用$ digest,它将不会隐式刷新$ applyAsync()队列。


〔实施例:


  $scope.$applyAsync(function () {
                if (!authService.authenticated) {
                    return;
                }

                if (vm.file !== null) {
                    loadService.setState(SignWizardStates.SIGN);
                } else {
                    loadService.setState(SignWizardStates.UPLOAD_FILE);
                }
            });


参考文献:


1.Scope。$ applyAsync()与Scope。在AngularJS 1.3中的$ evalAsync()[112]



  1. AngularJs Docs


其它参考21


类似于上面的答案,但这对我忠实...
在服务中添加:[113]


    //sometimes you need to refresh scope, use this to prevent conflict
    this.applyAsNeeded = function (scope) {
        if (!scope.$$phase) {
            scope.$apply();
        }
    };

其它参考22


您可以使用



  $timeout



防止错误。



 $timeout(function () {
                        var scope = angular.element($("#myController")).scope();
                        scope.myMethod();
                        scope.$scope();
                    },1);

其它参考23


发现这个:https://coderwall.com/p/ngisma其中Nathan Walker(在页面底部附近)建议$ rootScope中的装饰器来创建funcsafeApply,代码:[114]


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

其它参考24


这将解决您的问题:


if(!$scope.$$phase) {
  //TODO
}