首页 文章

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

提问于
浏览
794

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

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

错误:$ digest已在进行中

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

25 回答

  • 9

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

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

    唯一有效的方法是:

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

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

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

    在CoffeeScript中,它甚至更漂亮:

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

    什么's the issue with this? Is there an alternative that won' t创建一个FOUT? $safeApply看起来很好,但也使用 $$phase 检查方法 .

  • 28

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

    试试这个:

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

    首先,不要这样修复它

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

    If you are using underscore or lodash, you can use defer():

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

    这将解决您的问题:

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

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

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

    或者如果你有下划线:

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

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

  • 315

    安全 $apply 的最短形式是:

    $timeout(angular.noop)
    
  • 638

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

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

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

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

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

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

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

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

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

  • -3

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

    这里的许多答案包含很好的建议,但也可能导致混淆 . 简单地使用 $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的一个孤立部分(因为如果没有正在进行新的 $apply 将被触发),那仍然不如 $scope.$digest 好,但这是你执行时的最佳解决方案如果将同步执行或不同步执行,您无法知道它的功能,例如在获取可能缓存的资源之后:有时这将需要对服务器进行异步调用,否则资源将在本地同步获取 .

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

  • 32

    从最近与Angular家伙讨论这个话题: For future-proofing reasons, you should not use $$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,其中defaults to 0 if delay isn't set

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

  • 86

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

    您可以通过选中 $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 Anti Patterns

    不要这样做(!$ scope . $$阶段)$ scope . $ apply(),这意味着你的$ scope . $ apply()在调用堆栈中不够高 .

  • 14

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

  • 5

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

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

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

    防止此错误的方法是确保调用 $apply 的代码异步运行 . 这可以通过在 $timeout 调用中运行代码并将延迟设置为 0 (这是默认值)来完成 . 但是,调用 $timeout 中的代码会删除调用 $apply 的必要性,因为$ timeout将自行触发另一个 $digest 循环,这将反过来执行所有必要的更新等 .

    Solution

    简而言之,而不是这样做:

    ... 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 ,因为它会大致相同事情 .

  • 3

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

  • 2

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

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

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

    我和第三方脚本(例如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();
    
      }]
    )
    
  • 11

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

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

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

    在我的情况下, ng-click 更改范围内的变量,并且该变量的$ watch更改其他必须为 $applied 的变量 . 最后一步导致错误"digest already in progress" .

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

    因此,看起来如果由于Angular中的其他一些变化,digest将继续运行,那么你需要做的就是这样做 .

  • 261

    您可以使用

    $超时

    防止错误 .

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

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

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

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

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

    尝试使用

    $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

相关问题