我发现自从在angular中构建应用程序以来,我越来越需要手动将页面更新到我的范围。

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

错误:$digest已在进行中

有人知道如何避免这种错误或以不同的方式实现相同的事情吗?


当前回答

安全$apply的最短形式是:

$timeout(angular.noop)

其他回答

从最近与Angular团队就这个主题进行的讨论中可以看出:出于防将来的原因,您不应该使用$$phase

当被问及“正确”的做法时,答案是当前

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

我最近在编写angular服务来包装facebook、谷歌和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的延迟参数是可选的,如果未设置,则默认值为0($timeout调用$browser.deffer,如果没有设置延迟,则默认为0)

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

您也可以使用evalAsync。它将在摘要完成后的某个时间运行!

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

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

根据文档,$apply基本上做到了这一点:

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

在我的例子中,ng单击会更改范围内的变量,而该变量上的$watch会更改必须应用$apply的其他变量。最后一步导致错误“摘要已在进行中”。

通过在watch表达式中将$apply替换为$eval,作用域变量将按预期更新。

因此,如果因为Angular中的一些其他变化,digest将以任何方式运行,那么您需要做的就是$eval‘ing。

这里的许多答案都包含很好的建议,但也可能导致混淆。简单地使用$timeout既不是最好的解决方案,也不是正确的解决方案。此外,如果您关注性能或可伸缩性,请务必阅读。

你应该知道的事情

$$阶段对框架是私有的,这有充分的理由。$timeout(回调)将等待当前摘要周期(如果有)完成,然后执行回调,然后在结束时运行完整的$apply。$timeout(callback,delay,false)将执行相同的操作(在执行回调之前有一个可选的延迟),但不会触发$apply(第三个参数),如果您没有修改Angular模型($scope),则会节省性能。$范围$apply(callback)调用$rootScope等$digest,这意味着它将重新划分应用程序的根范围及其所有子范围,即使您在一个独立的范围内。$范围$digest()将简单地将其模型同步到视图,但不会消化其父范围,这可以在使用隔离范围(主要来自指令)处理HTML的隔离部分时节省大量性能$摘要不接受回调:执行代码,然后进行摘要。$范围$angularjs 1.2引入了evalAsync(回调),它可能会解决大部分问题。请参考最后一段了解更多信息。如果你得到了$digest已经在进行中的错误,那么你的架构是错误的:要么你不需要重新定义你的范围,要么你不应该负责这个(见下文)。

如何构造代码

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

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中都添加了一个新的强大方法。基本上,如果发生回调,它将在当前摘要周期内执行回调,否则新的摘要周期将开始执行回调。

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

在这些情况下,以及所有其他情况下$范围$$阶段,请确保使用$scope$evalAsync(回调)

在为我们创建一个可重用的$safeApply函数方面,yearmomo做得很好:

https://github.com/yearofmoo/AngularJS-Scope.SafeApply

用法:

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