AngularJS:在调用$ scope时防止错误$ digest正在进行中。$ apply()

问题描述 投票:816回答:25

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

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

错误:$ digest已在进行中

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

angularjs angularjs-scope angular-digest
25个回答
653
投票

不要使用这种模式 - 这最终会导致比它解决的更多错误。即使你认为它固定了一些东西,它也没有。

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

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

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

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

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


来自评论:@anddoutoi

angular.js Anti Patterns

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

11
投票

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

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

10
投票

首先,不要这样修复它

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

9
投票

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

试试这个:

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

5
投票

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

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

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


4
投票

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

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

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

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


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

4
投票

尝试使用

$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() vs. Scope.$evalAsync() in AngularJS 1.3

  1. AngularJs Docs

3
投票

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

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

2
投票

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

根据docs$apply基本上这样做:

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

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

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

因此,似乎如果由于Angular中的其他一些变化而无法正在运行摘要,那么$eval'ing就是你需要做的。


2
投票

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


1
投票

了解Angular文件调用检查$$phaseanti-pattern,我试图让$timeout_.defer工作。

超时和延迟方法在{{myVar}}中创建了一个未解析的FOUT内容。对我来说这是不可接受的。让我没有多少被教条地告诉我,某些东西是黑客,而没有一个合适的选择。

唯一有效的方法是:

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

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

“做消化,除非已经发生了”

在CoffeeScript中,它甚至更漂亮:

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

这有什么问题?有没有不会创建FOUT的替代方案? $safeApply看起来很好,但也使用$$phase检查方法。


656
投票

从最近与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的延迟参数是可选的,如果未设置则默认为0($timeout调用$browser.defer defaults to 0 if delay isn't set

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


1
投票

这是我的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']);
};

1
投票

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

0
投票

类似于上面的答案,但这已忠实地为我...在服务添加:

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

0
投票

您可以使用

$timeout

防止错误。

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

-3
投票

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

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

-7
投票

这将解决您的问题:

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

321
投票

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

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

或者如果你有lodash:

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

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


265
投票

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

你应该知道的事情

  • $$phase是该框架的私有部分,这是有充分理由的。
  • $timeout(callback)将等到当前的摘要周期(如果有的话)完成,然后执行回调,然后在最后运行一个完整的$apply
  • $timeout(callback, delay, false)将执行相同的操作(在执行回调之前有一个可选的延迟),但如果你没有修改Angular模型($ scope),则不会触发$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部分(因为如果没有正在进行的话会触发新的$scope.$digest),那仍然不如$apply那么好,但这是你执行时的最佳解决方案如果将同步执行或不同步执行,您无法知道它的功能,例如在获取可能缓存的资源之后:有时这将需要对服务器进行异步调用,否则资源将在本地同步获取。

在这些情况下和你有!$scope.$$phase的所有其他情况下,一定要使用$scope.$evalAsync( callback )


87
投票

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

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

32
投票

http://docs.angularjs.org/error/$rootScope:inprog

当您调用$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...

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

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


32
投票

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

  }]
)

28
投票

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


14
投票

最安全的$apply形式是:

$timeout(angular.noop)
© www.soinside.com 2019 - 2024. All rights reserved.