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

时间:2012-10-04 14:07:32

标签: angularjs angularjs-scope angular-digest

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

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

  

错误:$ digest已在进行中

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

28 个答案:

答案 0 :(得分:652)

最近与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 :(得分:647)

  

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

您可以通过选中$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()在调用堆栈中不够高。
  2.   

答案 2 :(得分:320)

摘要周期是同步通话。在完成之前,它不会控制浏览器的事件循环。有几种方法可以解决这个问题。解决这个问题最简单的方法是使用内置的$ 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结束。

答案 3 :(得分:264)

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

你应该知道的事情

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

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

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

  • $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 )

答案 4 :(得分:87)

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

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

答案 5 :(得分: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();

  }]
)

答案 6 :(得分: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...

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

除非有人意识到使用$timeout超过$apply会有一些影响力的劣势,否则我不明白为什么你不能总是使用$timeout(零延迟)而不是{ {1}},因为它会做同样的事情。

答案 7 :(得分:28)

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

答案 8 :(得分:14)

安全$apply的最短形式是:

$timeout(angular.noop)

答案 9 :(得分:11)

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

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

答案 10 :(得分:9)

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

试试这个:

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

答案 11 :(得分:9)

首先,不要这样修复

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

答案 12 :(得分:5)

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

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

  

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

答案 13 :(得分: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

答案 14 :(得分:4)

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

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

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

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


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

答案 15 :(得分: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);

答案 16 :(得分:2)

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

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

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

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

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

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

答案 17 :(得分:2)

使用$scope.$$phase || $scope.$apply();代替

答案 18 :(得分: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']);
};

答案 19 :(得分: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);
  }
}

答案 20 :(得分:1)

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

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

每次唯一有效的方法是:

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

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

  

&#34;除非已经发生了消化,否则请进行消化&#34;

在CoffeeScript中,它甚至更漂亮:

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

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

答案 21 :(得分:1)

当我禁用调试器时,该错误不再发生。 就我而言,是因为调试器停止了代码执行。

答案 22 :(得分:0)

类似于上面的答案,但这对我忠实地工作...... 在服务中添加:

throw new IllegalArgumentException("edge " + e + " has negative weight");

答案 23 :(得分:0)

您可以使用

  

$timeout

防止出错。

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

答案 24 :(得分:0)

这个问题基本上是在我们要求摘要运行摘要循环的时候出现的,即使它的过程正在为理解的角度创建问题。控制台中的后果异常。
1.在$ timeout函数中调用scope。$ apply()没有任何意义,因为在内部它是相同的。
2.该代码具有Vanilla JavaScript函数,因为其本机未定义角度,即setTimeout
3.为此,您可以使用
      
if(!scope。$$ phase){
            scope。$ evalAsync(function(){
           
});            }

答案 25 :(得分:0)

        let $timeoutPromise = null;
        $timeout.cancel($timeoutPromise);
        $timeoutPromise = $timeout(() => {
            $scope.$digest();
        }, 0, false);

这是避免此错误并避免$ apply

的好方法

如果基于外部事件进行调用,则可以将其与debounce(0)结合使用。上面是我们正在使用的“反跳”以及完整的代码示例

.factory('debounce', [
    '$timeout',
    function ($timeout) {

        return function (func, wait, apply) {
            // apply default is true for $timeout
            if (apply !== false) {
                apply = true;
            }

            var promise;
            return function () {
                var cntx = this,
                    args = arguments;
                $timeout.cancel(promise);
                promise = $timeout(function () {
                    return func.apply(cntx, args);
                }, wait, apply);
                return promise;
            };
        };
    }
])

和代码本身以侦听某些事件,并仅在所需的$ scope上调用$ digest

        let $timeoutPromise = null;
        let $update = debounce(function () {
            $timeout.cancel($timeoutPromise);
            $timeoutPromise = $timeout(() => {
                $scope.$digest();
            }, 0, false);
        }, 0, false);

        let $unwatchModelChanges = $scope.$root.$on('updatePropertiesInspector', function () {
            $update();
        });


        $scope.$on('$destroy', () => {
            $timeout.cancel($update);
            $timeout.cancel($timeoutPromise);
            $unwatchModelChanges();
        });

答案 26 :(得分:-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;
      }
    ]);
  }
]);

答案 27 :(得分:-7)

这将解决您的问题:

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